8.04. Разработка на Roblox
Разработка на Roblox
Платформа Roblox представляет собой экосистему, объединяющую пользователей и разработчиков для создания, публикации и взаимодействия с виртуальными мирами. Её ключевая особенность — возможность разработки трёхмерных игр без необходимости владения профессиональными 3D-редакторами или сложными игровыми движками. Основным инструментом для разработки выступает Roblox Studio — приложение, предоставляющее доступ к редактору сцены, системе физики, средствам визуализации и полнофункциональному языку программирования Lua.
Разработка на Roblox ориентирована как на начинающих, так и на опытных программистов. Низкий порог входа позволяет освоить основы геймдева уже на начальных этапах обучения, однако масштабируемость платформы и поддержка скриптования открывают возможности для реализации сложных механик, многопользовательского взаимодействия и даже экономических систем.
Основы
Система Roblox
Roblox — платформа с клиент-серверной архитектурой и интегрированной средой разработки.
Она состоит из трёх уровней:
| Уровень | Роль | Примеры компонентов |
|---|---|---|
| Платформа (Roblox Cloud) | Хостинг, аутентификация, платёжная система, DataStore, DevHub | Marketplace, DataStoreService, Developer Products |
| Сервер игры (Game Server) | Выполняет авторитетную логику: движение, коллизии, экономика | Workspace, ServerScriptService, Players, физика |
| Клиент (Roblox Player) | Отображает игру, обрабатывает ввод, показывает GUI | StarterGui, LocalScript, рендер, звук |
Важно: при запуске игры в Studio вы запускаете локальный сервер + один клиент — это эмуляция реального окружения. Но архитектура остаётся прежней: сервер и клиент — разные процессы.
Roblox Cloud - это глобальная распределённая система, управляемая Roblox Corporation. Не подвергается прямому влиянию разработчика игры, но предоставляет сервисы через API.
Roblox построена по принципу разделения ответственности между облачной инфраструктурой, игровым сервером и клиентом. Эта структура гарантирует безопасность, масштабируемость и согласованность состояния во всех сессиях.
Платформа (Roblox Cloud)
Это глобальная распределённая система, управляемая Roblox Corporation. Не подвергается прямому влиянию разработчика игры, но предоставляет сервисы через API.
Компоненты и их назначение
| Компонент | Описание | Доступ из игры | Примечания |
|---|---|---|---|
| Authentication & Identity | Управление аккаунтами, сессиями, ролевой моделью (RBAC) | game.Players.LocalPlayer.UserId, UserOwnsGamePassAsync() | Все Player объекты содержат только публичную информацию (Name, DisplayName); приватные данные (email, возраст) недоступны. |
| Game Hosting & Matchmaking | Выделение и управление игровыми серверами (Game Server), балансировка нагрузки, регион-маршрутизация | game.JobId, game.PlaceId | Сервер игры существует не дольше 6 часов (лимит idle-time); при завершении все данные должны быть сохранены в DataStore. |
| DataStoreService | Постоянное хранилище данных игроков и игры. Реализует ACID-подобную семантику с ограничениями. | DataStoreService:GetDataStore() | Три типа: GlobalDataStore, OrderedDataStore, LegacyDataStore. Ограничения: 60 чтений/записей в секунду на ключ, 4 MB на значение, 5 ключей в UpdateAsync за раз. |
| MarketplaceService | Обработка покупок через Robux, возвратов, проверки владения Game Pass. | MarketplaceService:PromptPurchase(), .PromptPurchaseFinished | Все операции — асинхронные. Подтверждение приходит только после финальной авторизации на сервере Roblox. |
| DevHub & Creator Dashboard | Система управления ассетами, монетизацией, аналитикой, Developer Products. | Только вне игры (веб-интерфейс) | Регистрация Developer Product требует: уникального ProductId, названия, описания, цены. После публикации — изменение цены невозможно (только создание нового продукта). |
| UGC & Asset Delivery | Хостинг и доставка пользовательских ассетов (модели, анимации, текстуры). | rbxassetid://..., rbxthumb://... | Все ассеты проходят модерацию. Использование непроверенных внешних ресурсов (http://) запрещено политикой. |
| Telemetry & Analytics | Сбор метрик: retention, DAU, crash reports, performance stats. | Только через встроенные события (.ChildAdded, HttpService запрещён для отправки) | Данные доступны разработчику в DevHub. Прямой экспорт (например, в Google Analytics) блокируется. |
Важные ограничения платформы
- Нет доступа к файловой системе — даже для временных файлов.
- Нет прямого HTTP-выхода —
HttpServiceразрешён только для ограниченного набора whitelisted-хостов (например, Discord webhooks — только с включённой опцией в Settings). - Время на сервере — UTC, без доступа к локальному времени игрока.
- Память ограничена: ~1.5–2.5 GB RAM на сервер (в зависимости от региона и тарифа). Переполнение → OOM-kill.
Сервер игры (Game Server)
Это экземпляр движка Roblox, развернутый в облаке или локально (в Studio). Выполняет авторитетную (authoritative) логику — то есть любое его решение считается окончательным.
Компоненты
| Компонент | Назначение | Примеры |
|---|---|---|
| DataModel | Корень иерархии объектов. Содержит: Workspace, Players, Lighting, ReplicatedFirst и др. | game, DataModel — синонимы. |
| Workspace | Сцена физического мира. Все BasePart, Model, Terrain, персонажи — здесь. | Физика (PhysX), рендер, коллизии. |
| Players | Коллекция подключённых игроков. Управляет жизненным циклом Player и Character. | game.Players.PlayerAdded, player.CharacterAdded. |
| ServerScriptService | Контейнер только для серверных скриптов (Script). Автоматически запускается при загрузке. | Логика победы, экономика, инициализация уровней. |
| ReplicatedStorage | Общий контейнер для объектов, доступных и серверу, и клиентам. | Каталоги предметов, общие ModuleScript, RemoteEvent. |
| PhysicsService | Управление группами коллизий, физическими параметрами. | CreateCollisionGroup, CollisionGroupSetCollidable. |
| LogService | Централизованный логгер для сервера. | LogService.MessageOut:Connect(...). |
Жизненный цикл сервера
-
Инициализация
Загружаются:DataModel,Workspace,Lighting,ReplicatedStorage,ServerScriptService. ВыполняютсяScriptв порядке создания. -
Ожидание игроков
Сервер работает в фоне, обрабатывая события (например, таймеры), но не запускаетCharacterдо подключения. -
Подключение игрока
- Создаётся
Playerвgame.Players. - Вызывается
PlayerAdded. - Клонируются
StarterPlayer,StarterGui,StarterPack→ вplayer. - Сервер создаёт
player.Character(если не отключеноplayer:LoadCharacter()).
- Создаётся
-
Игровой процесс
- Физика, события, скрипты работают синхронно с тиком движка (~30–60 FPS).
- Сервер отправляет обновления клиентам через репликацию (изменения свойств, событий, объектов).
-
Отключение игрока
Characterуничтожается →player.Character = nil.- Вызывается
PlayerRemoving. - Данные игрока сохраняются (если предусмотрено).
-
Завершение сессии
Сервер останавливается через 6 часов бездействия или при ручной остановке. Все несохранённые данные теряются.
Ключевые принципы
-
Репликация идёт только «сверху вниз»:
Server → Client, но не наоборот — кроме специально разрешённыхRemoteEvent/RemoteFunction. -
Сервер не имеет GUI — ни
ScreenGui, ниTextLabel, ни рендер.
Всё, что видит игрок, — результат репликации на клиент. -
Нет доступа к
LocalPlayer—game.Players.LocalPlayerвсегдаnilна сервере.
Клиент (Roblox Player)
Это приложение, установленное на устройстве игрока (Windows, macOS, iOS, Android, Xbox, VR). Отвечает за локальную интерактивность и рендер.
Компоненты
| Компонент | Назначение | Примеры |
|---|---|---|
| Renderer | 3D-рендеринг сцены (на основе Enlighten для освещения, Physically Based Rendering). | Тени, отражения, пост-обработка. |
| InputManager | Обработка клавиатуры, мыши, тача, геймпада. | UserInputService.InputBegan, ContextActionService. |
| SoundService | Воспроизведение звуков и музыки. | Sound:Play(), SoundService.RespectFilteringEnabled. |
| StarterGui | Шаблон GUI. Не отображается — клонируется в player.PlayerGui при входе. | ScreenGui, Frame, TextButton. |
| LocalScript | Клиентские скрипты. Должны быть в PlayerScripts, StarterGui, Backpack. | Анимации камеры, HUD, обработка кликов. |
| PlayerScripts | Специальный контейнер в StarterPlayerScripts. Загружается после Character. | Animate.lua, пользовательские LocalScript. |
Жизненный цикл клиента
-
Загрузка
Подключается к Game Server, запрашиваетDataModel. -
Инициализация
ПринимаетWorkspace,Lighting, клонируетStarter*→ в свой контекст. -
Создание персонажа
После полученияplayer.Character— запускаютсяLocalScriptвPlayerScripts. -
Интерактивность
- Обработка ввода → отправка
RemoteEvent:FireServer(). - Обновление GUI в ответ на
RemoteEvent.OnClientEvent. - Рендер в реальном времени.
- Обработка ввода → отправка
-
Отключение
При выходе — остановка всех скриптов, очистка памяти.
Ограничения клиента
-
Нет доступа к другим игрокам, кроме
LocalPlayer.
game.Players:GetPlayers()возвращает всех, ноplayer.Characterдругих игроков — только позиция и ориентация (безHumanoid.Health, если не реплицируется). -
Нет прямого изменения
Workspace— попыткаpart.Position = ...вLocalScriptвызовет ошибкуattempt to index nilили игнорируется. -
Локальные данные не сохраняются —
PlayerGui,Backpack,leaderstatsуничтожаются при выходе, если не синхронизированы с сервером. -
Девконсоль (F9) позволяет:
- Выполнять Lua-код (но только в локальной сессии);
- Изменять свойства объектов (но только нереплицируемые);
- Не даёт доступа к
ServerScriptService,DataStore.
Взаимодействие уровней
Пример: покупка предмета за монеты
| Этап | Платформа | Сервер | Клиент |
|---|---|---|---|
| 1. Инициация | — | — | button:Click() → BuyEvent:FireServer("Item_001", txId) |
| 2. Валидация | — | Проверка txId, баланса, наличия предмета | — |
| 3. Транзакция | — | CurrencyManager:Spend(); Inventory:Unlock(); DataStore:UpdateAsync() | — |
| 4. Подтверждение | — | BuyEvent:FireClient(player, "Success") | Получает событие → обновляет GUI |
| 5. Сохранение | DataStore принимает запись, реплицирует в резервные ноды | Операция завершена | — |
⚠️ Ни один этап не может быть пропущен. Пропуск валидации на сервере = уязвимость к читерству.
Порядок инициализации
Процесс запуска делится на три фазы: инициализация мира (server-only), подключение игрока (server), инициализация клиента (client). Каждая фаза имеет строгий порядок и набор событий.
Фаза 1. Инициализация сервера (до подключения игроков)
Происходит при запуске game, локально в Studio (Play) или в облаке (Production Server). Выполняется один раз за сессию сервера.
Шаг 1.1. Загрузка DataModel
- Создаётся корневой объект
DataModel(game). - Инициализируются системные сервисы:
WorkspacePlayersLightingReplicatedStorageServerScriptServiceSoundServiceHttpService(если разрешён)- и др.
⚠️
ReplicatedFirst,StarterGui,StarterPlayer— не создаются автоматически. Они появляются только если присутствуют в.rbxlxили были добавлены вручную.
Шаг 1.2. Выполнение Script в ServerScriptService
- Скрипты запускаются в порядке их создания (не алфавитном, не по имени).
- Параллельное выполнение:
- Если скрипт содержит
while true do task.wait(), он не блокирует загрузку других скриптов. task.spawn()иcoroutineпозволяют инициировать фоновые процессы.
- Если скрипт содержит
- Доступные сервисы:
game:GetService("Workspace")✅game.Players:GetPlayers()→{}(пустая таблица)game.Players.LocalPlayer→nil(сервер не имеет LocalPlayer)
Шаг 1.3. Инициализация Workspace
- Физика ещё не запущена:
Workspace:IsLoaded()→false. - Коллизии, гравитация,
Touched— неактивны. - Можно создавать объекты (
Part,Model), но они не участвуют в симуляции, покаWorkspace.Loadedне станетtrue.
Шаг 1.4. Событие Workspace.Loaded
- Флаг
Workspace.Loaded = trueустанавливается. - Запускается физический движок (PhysX).
- Объекты в
Workspaceстановятся физическими:Anchored = false→ начинают падать.Touched,TouchEnded— активны.BasePart:GetConnectedParts()— работает.
✅ Рекомендуется: откладывать физически значимые операции (например,
:MoveTo(),ApplyImpulse()) доWorkspace.Loaded:Wait().
Шаг 1.5. Инициализация Lighting
- Устанавливаются параметры по умолчанию (время суток, туман, Ambient).
- После
Workspace.Loaded— начинает влиять на рендер.
Фаза 2. Подключение игрока (серверная сторона)
Происходит при первом подключении игрока к серверу. Может повторяться для каждого игрока.
Шаг 2.1. Событие Players.PlayerAdded
- Создаётся объект
player(классPlayer) и добавляется вgame.Players. - Свойства
player:player.Name,player.DisplayName✅player.UserId✅player.Character→nilplayer.PlayerGui→nilplayer.Backpack→nil
⚠️ Попытка доступа к
player.Character.Humanoidздесь →attempt to index nil.
Шаг 2.2. Клонирование Starter-контейнеров
Последовательность строго фиксирована:
| Порядок | Действие | Куда клонируется |
|---|---|---|
| 1 | StarterPlayerScripts | → player.PlayerScripts |
| 2 | StarterCharacterScripts | → player.CharacterScripts (только при создании Character) |
| 3 | StarterPlayer | → player (свойства, атрибуты, скрипты) |
| 4 | StarterGui | → player.PlayerGui (создаётся пустой PlayerGui, затем клонируются дочерние элементы) |
| 5 | StarterPack | → player.Backpack (создаётся Backpack, затем клонируются инструменты) |
🔍 Важно:
StarterGuiиStarterPackне клонируются напрямую — создаются контейнеры (PlayerGui,Backpack), и в них копируются дети.- Если
StarterGuiпуст —player.PlayerGuiвсё равно создаётся (как пустая папка).- Клонирование не включает
LocalScriptвStarterPlayer→ они становятсяScriptвplayer.PlayerScripts.
Шаг 2.3. Инициализация PlayerGui и Backpack
player.PlayerGuiиplayer.Backpackстановятся не-nil.- Но: GUI ещё не отображается — рендер не начался.
Шаг 2.4. Создание персонажа (Character)
- Вызывается внутренне
player:LoadCharacter()(еслиplayer.Character=nilи не отключено в настройках). - Происходит асинхронно — обычно через 0.1–0.5 сек после
PlayerAdded. - Этапы создания персонажа:
- Создаётся
Model→player.Character. - В него клонируется R15/R6 rig (в зависимости от настроек).
- Применяются ассеты (одежда, лицо, тело — из аккаунта игрока).
- Запускается
Animator,Humanoid,HumanoidRootPart. - Вызывается событие
player.CharacterAdded.
- Создаётся
⚠️
player.CharacterAddedне гарантирует, что:
HumanoidRootPartуже существует (может быть задержка из-за загрузки мешей);Humanoid.Health > 0(иногдаHumanoidинициализируется сHealth = 0, затемHealth = 100).
Шаг 2.5. Событие player.CharacterAdded
- Сигнализирует, что
player.Character≠nilи содержит базовую иерархию. - Рекомендуемая практика:
player.CharacterAdded:Connect(function(character)
local hrp = character:WaitForChild("HumanoidRootPart", 5)
if not hrp then return end
local humanoid = character:WaitForChild("Humanoid", 5)
if not humanoid or humanoid.Health <= 0 then
-- Дождаться полной инициализации
humanoid.HealthChanged:Wait()
end
-- Теперь можно: hrp.Position, humanoid:MoveTo(), и т.д.
end)
Фаза 3. Инициализация клиента (локальная машина игрока)
Запускается после того, как сервер отправил клиенту DataModel и player.Character.
Шаг 3.1. Получение DataModel на клиенте
- Клиент получает синхронизированную копию
Workspace,Lighting,ReplicatedStorage. game.ReplicatedFirstклонируется →game.StarterGui→PlayerGui(локальная копия).
Шаг 3.2. Создание LocalPlayer
game.Players.LocalPlayer→ ссылка на собственного игрока.- Доступ к другим игрокам:
game.Players:GetPlayers()✅player.Characterдругих игроков — толькоPrimaryPart,Humanoid.Health(если разрешено),Position(реплицируется).
Шаг 3.3. Запуск LocalScript
LocalScriptвыполняются только в следующих контейнерах:PlayerScriptsPlayerGuiи его детиBackpackи его детиStarterPlayerScripts(клонируется вPlayerScripts)
LocalScriptвWorkspace,ReplicatedStorage,ServerScriptService— игнорируются.
Шаг 3.4. Событие PlayerGui.Initialized
- Гарантирует, что весь GUI загружен и отображается.
- Аналог
CharacterAddedдля интерфейса:player.PlayerGui.Initialized:Connect(function()
-- Безопасно искать ScreenGui, Frame, Button
end)
Шаг 3.5. Запуск StarterCharacterScripts на клиенте
LocalScript, клонированные изStarterCharacterScripts, запускаются послеCharacterAdded.- Именно здесь работает
Animate.lua— анимации движения/прыжка.
Доступность объектов по времени
| Временная метка | player.Character | player.PlayerGui | Humanoid | HumanoidRootPart | LocalPlayer | Workspace.Loaded |
|---|---|---|---|---|---|---|
PlayerAdded | nil | nil | — | — | nil (сервер) / ✅ (клиент) | ? |
После клонирования StarterGui | nil | ✅ (пустой) | — | — | ✅ | ? |
CharacterAdded | ✅ (Model) | ✅ | ✅ (но Health может быть 0) | ✅ (иногда с задержкой) | ✅ | ✅ |
Humanoid.Health > 0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
🔎 Примечание:
Workspace.Loadedобычно происходит до первогоPlayerAdded, но в сложных сценах (большие мешы, скрипты инициализации) — может быть позже.
Типичные ошибки и как их избежать
| Ошибка | Причина | Решение |
|---|---|---|
attempt to index nil with 'Humanoid' | Доступ к player.Character.Humanoid в PlayerAdded | Использовать player.CharacterAdded:Wait() или :Connect() |
| GUI не отображается | LocalScript ищет ScreenGui сразу после PlayerAdded | Дождаться player.PlayerGui.Initialized или использовать player.PlayerGui:WaitForChild("MyGui") |
| Скрипт не запускается | LocalScript размещён в ReplicatedStorage | Перенести в StarterGui или StarterPlayerScripts |
RemoteEvent не срабатывает | RemoteEvent создан на клиенте, а не в ReplicatedStorage | Создавать RemoteEvent в ReplicatedStorage на сервере до подключения игроков |
| Предмет не появляется в инвентаре | Попытка tool.Parent = player.Backpack до PlayerAdded | Выполнять в PlayerAdded или CharacterAdded |
Как проверить порядок самому?
Добавьте в ServerScriptService этот скрипт:
print("[Init] Server starting...")
game.Workspace.Loaded:Wait()
print("[Init] Workspace.Loaded = true")
game.Players.PlayerAdded:Connect(function(player)
print("[Event] PlayerAdded:", player.Name)
print(" → Character =", player.Character)
print(" → PlayerGui =", player.PlayerGui and "exists" or "nil")
player.CharacterAdded:Connect(function(char)
print("[Event] CharacterAdded for", player.Name)
print(" → Humanoid =", char:FindFirstChild("Humanoid") and "exists" or "nil")
print(" → HRP =", char:FindFirstChild("HumanoidRootPart") and "exists" or "nil")
end)
task.delay(0.2, function()
if player and player.Parent then
print("[Check 0.2s] Char =", player.Character ~= nil)
end
end)
task.delay(1, function()
if player and player.Parent then
local char = player.Character
if char then
print("[Check 1s] HRP =", char:FindFirstChild("HumanoidRootPart") ~= nil)
local hum = char:FindFirstChild("Humanoid")
print("[Check 1s] Humanoid.Health =", hum and hum.Health or "nil")
end
end
end)
end)
Instance
Instance — базовый класс (в терминах ООП — суперкласс), от которого наследуются все объекты в среде Roblox. Это не просто «контейнер с данными»: это активный субъект, участвующий в репликации, сериализации, событийной модели и управлении памятью.
Техническое определение
- Тип:
DataType.Instance(в Lua —typeof(obj) == "Instance"). - Наследование: иерархия классов реализована в C++ ядре движка; в Lua доступна только через
obj.ClassName,obj:IsA("Part"). - Уникальный идентификатор: каждому
Instanceприсваивается GUID при создании (obj:GetDebugId()→ строка вида"1234567890"). Не путать сobj.Name(не уникален) илиobj.UserId(только дляPlayer).
✅ Проверка:
local part = Instance.new("Part")
print(typeof(part)) -- "Instance"
print(part.ClassName) -- "Part"
print(part:IsA("BasePart"))-- true (Part <: BasePart <: Instance)
Структура Instance
Каждый экземпляр состоит из четырёх взаимосвязанных компонентов. Нарушение целостности одного — делает объект неработоспособным.
| Компонент | Назначение | Реализация | Доступность |
|---|---|---|---|
| Класс (Class Info) | Определяет метаданные: список свойств, методов, событий, наследование. | Загружается из rbxmx-манифестов при старте движка. Неизменяем. | Только для чтения (ClassName, IsA(), GetFullName()) |
| Состояние (State) | Текущие значения свойств (Position, Name, Enabled и т.д.). | Хранится в памяти; сериализуется в .rbxlx/.rbxl. | Чтение/запись через obj.Property = value |
| Поведение (Methods & Events) | Логика: методы (:Clone()) и точки расширения (.Touched). | Часть класса; методы — вызовы C++ функций. | Вызов через obj:Method() или obj.Event:Connect() |
| Контекст (Hierarchy) | Положение в дереве (Parent, дети, Ancestors). | Управляется через Parent-ссылку; критично для репликации и сборки мусора. | Изменение через obj.Parent = parent |
⚠️ Важно:
- Свойства, методы и события не определяются динамически (как в JavaScript). Они зашиты в класс.
- Попытка
part.NewProperty = 42не создаёт новое свойство — это создаёт локальную переменную в таблице Lua, не связанную сInstance.- Для хранения пользовательских данных используйте
obj:SetAttribute("key", value)илиFolderсIntValue/StringValue.
Класс, экземпляр, переменная
| Понятие | Что это | Где хранится | Жизненный цикл |
|---|---|---|---|
Класс (Part, Script) | Метаописание: какие свойства/методы есть, как сериализуется, как ведёт себя в физике. | В C++ библиотеке движка (rbx-core.dll и др.). Загружается один раз при старте. | Вечный — пока работает движок. |
Экземпляр (workspace.Ground) | Конкретный объект в сцене. Имеет состояние и позицию в иерархии. | В куче (heap) движка. Имеет ссылочный счётчик. | От Instance.new() / клонирования — до :Destroy() или сборки мусора. |
Переменная в скрипте (local p = workspace.Ground) | Ссылка (указатель) на экземпляр. Не содержит данных — только адрес. | В Lua-стеке/куче. | До выхода из области видимости или переприсваивания. |
Пример, демонстрирующий разницу:
-- 1. Создаём экземпляр
local part1 = Instance.new("Part")
part1.Name = "Cube"
part1.Parent = workspace -- ← объект попадает в сцену
-- 2. Переменная — ссылка
local ref1 = part1
local ref2 = workspace.Cube
print(part1 == ref1) -- true
print(part1 == ref2) -- true (один и тот же экземпляр)
-- 3. Клонирование создаёт **новый экземпляр**
local part2 = part1:Clone()
part2.Parent = workspace
part2.Name = "Cube2"
print(part1 == part2) -- false (разные объекты в памяти)
🔍 При отладке в Studio:
— Выделениеpart1иpart2в Explorer — разные элементы.
—print(part1, part2)→ разныеDebugId.
Свойства
Типы свойств
| Тип | Примеры | Особенности |
|---|---|---|
| Базовые (Base) | Name (string), Parent (Instance?), Archivable (bool) | Есть у всех Instance. Изменение Name не влияет на репликацию. |
| Класс-специфичные | Part.Size (Vector3), Script.Source (string), RemoteEvent.OnServerEvent (RBXScriptSignal) | Определены в классе. Недоступны в других классах (Part.Script → ошибка). |
| Сериализуемые | Почти все, кроме LocalScript.Disabled, Player.UserId | Сохраняются в .rbxlx. Несериализуемые — игнорируются при сохранении. |
| Реплицируемые | Part.Position, StringValue.Value, Player.DisplayName | Изменения отправляются клиентам. Не реплицируются: LocalScript, Player.UserId, Folder.ChildAdded и др. |
Доступ к свойствам
- Чтение:
obj.Property— всегда возвращает текущее значение (может бытьnil, если свойство не инициализировано). - Запись:
obj.Property = value— валидируется типом:part.Size = "hello" -- ошибка: "Unable to cast string to Vector3" - Отсутствующее свойство:
print(part.NonExistent) -- nil (без ошибки)
part.NonExistent = 5 -- игнорируется (не создаёт свойство)
Событие .Changed
Срабатывает после успешного изменения любого свойства (включая Name, Parent).
part.Changed:Connect(function(property)
print("Свойство", property, "изменено на", part[property])
end)
part.Size = Vector3.new(2, 2, 2)
-- Вывод: "Свойство Size изменено на 2, 2, 2"
part.Name = "NewCube"
-- Вывод: "Свойство Name изменено на NewCube"
⚠️ Не используйте
.Changedдля критичной логики (например, сохранения):
— Оно срабатывает при любом изменении, включая репликацию.
— Не гарантирует порядок относительно других событий.
Методы
Методы — вызовы C++ функций через Lua-обёртку. Они не копируются при клонировании — поведение определяется классом.
Наиболее важные методы:
| Метод | Назначение | Особенности |
|---|---|---|
:Clone() | Создаёт глубокую копию объекта и всех детей. | Возвращает новый экземпляр с Parent = nil. Требует obj.Archivable = true (по умолчанию true). |
:Destroy() | Уничтожает объект и всех детей. | Немедленно удаляет из памяти; события отключаются; Parent = nil. Безопасно вызывать повторно. |
:FindFirstChild(name, recursive?) | Поиск ребёнка по имени. | Не вызывает ошибку, если не найден → возвращает nil. recursive = true — поиск во всей подветке. |
:WaitForChild(name, timeout?) | Блокирующий поиск (с ожиданием). | Используется при инициализации (Character:WaitForChild("HumanoidRootPart")). При timeout → nil. |
:GetChildren() | Возвращает таблицу прямых детей. | Не рекурсивно. Порядок — как в Explorer (по времени создания). |
:IsDescendantOf(parent) | Проверка принадлежности к ветке. | Используется для проверки контекста (if tool:IsDescendantOf(player.Backpack) then). |
:ClearAllChildren() | Удаляет всех детей (но не сам объект). | Эквивалентно for _, c in ipairs(obj:GetChildren()) do c:Destroy() end. |
✅ Рекомендация:
Всегда используйте:Destroy(), а неobj.Parent = nil.
—obj.Parent = nilоставляет объект в памяти, если есть ссылки (утечка).
—:Destroy()гарантирует освобождение ресурсов.
События
События в Roblox — это экземпляры RBXScriptSignal. Они предоставляют два основных метода:
| Метод | Назначение | Возвращаемое значение |
|---|---|---|
:Connect(callback) | Подписка на событие. | RBXScriptConnection — объект соединения. |
:Wait() | Блокирующее ожидание события. | Значения, переданные в Fire(...). |
Пример работы с соединением:
local connection = workspace.Part.Touched:Connect(function(hit)
print("Коснулся", hit.Name)
end)
-- Отмена подписки
connection:Disconnect() -- событие больше не будет вызываться
-- Проверка состояния
print(connection.Connected) -- false после Disconnect()
⚠️ Утечки памяти:
— Если не вызвать:Disconnect(), соединение остаётся в памяти даже после:Destroy()объекта.
— Особенно критично вLocalScript: при выходе игрока соединения не отключаются автоматически.
Диагностика и отладка Instance
| Задача | Метод | Пример |
|---|---|---|
| Узнать класс | obj.ClassName | "Part" |
| Проверить тип | obj:IsA("BasePart") | true для Part, MeshPart |
| Полный путь | obj:GetFullName() | "Workspace.Ground" |
| Отладочный ID | obj:GetDebugId() | "123456789" (уникален в сессии) |
| Проверить, уничтожен ли | not obj or not obj.Parent | Ненадёжно; лучше отслеживать вручную |
| Проверить, клонируемый ли | obj.Archivable | true по умолчанию для большинства объектов |
🛠️ Совет: при отладке используйте
warn(obj:GetFullName(), obj.ClassName, obj:GetDebugId())— это выводит кликабельную ссылку в Output Studio.
Основные понятия
Instance
В Roblox всё — объект типа Instance:
Workspace,Part,Script,RemoteEvent,Player,Folder,DataModel.
Каждый Instance обладает:
- Классом — определяет возможности объекта (например, у
PartестьSize,Anchored; уRemoteEvent—FireServer,OnServerEvent). - Свойствами — данные, которые можно читать/писать (
Name,Parent,Transparency,Position). - Методами — действия, которые можно вызвать (
:Clone(),:Destroy(),:FindFirstChild()). - Событиями — сигналы, на которые можно подписаться (
.Touched,.Changed,MouseButton1Click).
⚠️ Не путайте:
- Класс — абстрактное описание (например,
Part).- Экземпляр — конкретный объект в сцене (например,
GroundвWorkspace).- Переменная в скрипте — ссылка на экземпляр, а не его копия.
Иерархия
Любой Instance имеет ровно одного родителя (Parent) и может иметь много детей.
-- Создание объекта и привязка к иерархии
local part = Instance.new("Part")
part.Name = "MyCube"
part.Parent = workspace -- ← важнейшая строка: без неё объект "висит в воздухе"
| Контекст | Как работает Parent |
|---|---|
| В Explorer | Дочерние объекты отступлены, скрытие родителя скрывает детей |
| В коде | game.Workspace.MyCube — синтаксический сахар для game:GetService("Workspace"):FindFirstChild("MyCube") |
| При клонировании | :Clone() копирует всю подветку, но отвязывает от родителя — нужно вручную задать Parent. |
✅ Пример ошибки:
local part = Instance.new("Part")
part.Position = Vector3.new(0, 10, 0)
part.Anchored = true
-- ... и ничего не происходитПричина:
part.Parent == nil. Объект создан, но не добавлен в сцену → не рендерится и не участвует в физике.
Контейнеры и сервисы
Что такое контейнер?
Контейнер — это Instance, который может содержать другие объекты.
Основные типы:
| Контейнер | Тип | Роль | Доступ |
|---|---|---|---|
Workspace | Model | Все видимые объекты мира | Сервер + клиент |
ReplicatedStorage | Folder | Данные и объекты для обмена | Сервер + клиент |
ServerScriptService | Folder | Серверные скрипты | Только сервер |
StarterGui | Folder | Шаблоны GUI для игроков | Только сервер (клонируется) |
Players | Players | Коллекция подключённых игроков | Сервер + клиент (только свой игрок) |
Сервис — специальный контейнер, предоставляющий API (например,
MarketplaceService,PhysicsService).
Доступ к сервисам:local ds = game:GetService("DataStoreService")
local phys = game:GetService("PhysicsService")
Как объект попадает в игру?
- Ручное размещение в Studio → сохраняется в
.rbxlx→ загружается сервером при старте. - Клонирование из
Starter*→ при заходе игрока. - Создание через
Instance.new()→ динамически, во время игры.
→ Только объекты с Parent ~= nil и находящиеся в Workspace, Lighting, Players и т.п. становятся активными.
Добавление скрипта к объекту
Этот раздел можно использовать как универсальный алгоритм для любого объекта: Part, Model, Tool, ScreenGui, ImageButton. Следуйте шагам строго — это гарантирует корректную работу и избегание распространённых ошибок.
Шаг 1. Создание объекта в сцене
Действия в Studio:
-
Откройте вкладку Home → нажмите Part
→ вWorkspaceпоявитсяPart(по умолчанию:Position = (0, 5, 0),Size = (2, 2, 2),Anchored = false). -
В Explorer найдите объект:
Workspace
└── Part ── [выделите его] -
В Properties задайте:
Name = "Button"(для идентификации в коде),Anchored = true(чтобы не падал),Color = BrickColor.new("Bright green")(визуальная обратная связь),CanCollide = false(чтобы игрок мог пройти сквозь него — опционально).
✅ Почему
Anchored = true?
ЕслиAnchored = false, объект подчиняется физике: упадёт, может улететь при столкновении → поведение непредсказуемо. Для интерактивных элементов (кнопок, триггеров) — почти всегдаtrue.
Шаг 2. Добавление скрипта к объекту
Действия в Studio:
- Убедитесь, что
Buttonвыделен в Explorer. - ПКМ по
Button→ Insert Object… → выберите Script
→ создаётсяScriptвнутриButton(в иерархии:Workspace.Button.Script).
⚠️ Критическое правило:
— Скрипт, помещённый внутрь объекта, имеет прямой доступ к нему черезscript.Parent.
— Это надёжнее, чемworkspace:FindFirstChild("Button"), потому что:
• не зависит от имени;
• не требует поиска;
• работает даже при переименовании объекта.
Что произошло технически:
- Создан экземпляр класса
Script(неLocalScript, неModuleScript). - Его свойство
Parentустановлено вButton. - Скрипт автоматически запущен (сервером), так как находится в
Workspace— зона видимости сервера.
Шаг 3. Базовый шаблон скрипта
Замените содержимое Script на следующий код — это универсальный шаблон, пригодный для 90 % простых интерактивных объектов:
-- Шаблон: интерактивный объект с реакцией на касание
-- Размещается внутри объекта (script.Parent = объект)
local object = script.Parent -- ← основное правило: привязка к родителю
local enabled = true -- флаг для безопасного отключения логики
-- 1. Проверка: объект существует и поддерживает события
if not object:IsA("BasePart") and not object:IsA("Model") then
warn("[Script] Ошибка: скрипт должен быть внутри Part/Model")
return
end
-- 2. Обработчик события
local function onTouched(hit)
if not enabled then return end
-- Проверка: коснулся ли игрок?
local character = hit:FindFirstChildOfClass("Model")
if not character then return end
local humanoid = character:FindFirstChild("Humanoid")
if not humanoid or humanoid.Health <= 0 then return end
-- Проверка: это не наш собственный объект (защита от self-touch)
if hit:IsDescendantOf(object) then return end
-- ✅ Здесь — ваша логика
print("Объект", object.Name, "активирован игроком", humanoid.Parent.Name)
-- Пример: смена цвета
object.BrickColor = BrickColor.Random()
-- Пример: вызов функции
activateEffect(object)
end
-- 3. Вспомогательная функция
local function activateEffect(target)
-- Изменение свойства
target.Transparency = 0.5
task.delay(0.2, function()
if enabled and target.Parent then
target.Transparency = 0
end
end)
-- Вызов метода
target:ScaleTo(Vector3.new(1.2, 1.2, 1.2), 0.2)
task.delay(0.2, function()
if enabled and target.Parent then
target:ScaleTo(Vector3.new(1, 1, 1), 0.2)
end
end)
end
-- 4. Подключение события
local connection = object.Touched:Connect(onTouched)
-- 5. Очистка при уничтожении объекта
script.Destroying:Connect(function()
enabled = false
if connection and connection.Connected then
connection:Disconnect()
end
end)
Разбор шаблона по блокам
| Блок | Зачем нужен | Что будет, если убрать |
|---|---|---|
local object = script.Parent | Прямая привязка к контексту. Надёжнее workspace.Button. | При переименовании/копировании — сломается. |
IsA("BasePart"/"Model") | Защита от размещения скрипта в Folder, IntValue и др. | Ошибка attempt to index nil при Touched. |
Проверка hit:FindFirstChildOfClass("Model") | Игнорировать касания частиц, Part, не принадлежащих персонажу. | Сработает на падающем мусоре, эффектах. |
hit:IsDescendantOf(object) | Защита от self-touch (например, при Anchored = false). | Объект может активировать сам себя. |
enabled + Destroying | Гарантированная отмена подписки. | Утечка памяти при удалении объекта. |
ScaleTo() вместо Size = ... | Плавное изменение (встроенный метод анимации). | Резкое изменение — режет глаз. |
Шаг 4. Расширение: другие типы взаимодействия
Тот же шаблон работает для любого события — нужно только заменить:
| Тип взаимодействия | Что изменить в шаблоне |
|---|---|
| Нажатие мышью (в GUI) | Объект: TextButton в ScreenGui; Событие: MouseButton1Click; Проверка: не нужна (GUI — только клиент) |
| Использование инструмента | Объект: Tool в Backpack; Событие: Activated; Место скрипта: внутри Tool → будет Script, а не LocalScript |
| Вход в зону (Region3) | Объект: Part как триггер; Логика: while true do task.wait(); if hrp.Position inside region then ...; Событие: не используется — polling |
| Клиентская анимация (камера, HUD) | Скрипт: LocalScript, размещённый в StarterGui/Button; Событие: MouseButton1Click |
Пример: кнопка в GUI (клиентская версия)
-- LocalScript внутри StarterGui/ScreenGui/TextButton
local button = script.Parent
local enabled = true
local function onClick()
if not enabled then return end
-- Изменение свойства объекта GUI
button.TextColor3 = Color3.new(1, 0, 0)
-- Вызов удалённой функции (безопасно!)
game.ReplicatedStorage.RemoteEvents.Action:FireServer("ButtonClick")
end
local connection = button.MouseButton1Click:Connect(onClick)
script.Destroying:Connect(function()
enabled = false
if connection then connection:Disconnect() end
end)
✅ Правило безопасности:
— Клиент может инициировать действие, но не выполнять его напрямую.
— Реальная логика (начисление монет, выдача предмета) — только послеFireServer→ серверная валидация.
События
События — это точки расширения, в которые можно «подключить» код.
-- Подписка на событие
workspace.Part.Touched:Connect(function(hit)
print("Столкнулся с", hit.Name)
end)
Как это работает:
- Происходит физическое столкновение → движок вызывает событие
.Touched. - Система ищет все активные
:Connect()на этом событии. - Для каждого — создаётся задача, выполняемая в порядке подписки.
✅ Важно:
:Connect()возвращает соединение (RBXScriptConnection) — его можно.Disconnect().- События не блокируют основной поток: обработчики запускаются асинхронно.
Игрок
player.Character — это объект типа Model, размещённый непосредственно в Workspace. Он представляет физическую форму игрока в мире: тело, анимации, коллизии, передвижение. Его наличие необязательно: игрок может быть в игре без персонажа (например, в интерфейсном меню), но любое взаимодействие с 3D-миром требует его загрузки.
Персонаж создаётся, существует и уничтожается в рамках строго определённого цикла, управляемого движком. На каждый этап приходится конкретное событие и состояние объекта.
-
Состояние
player.Character = nil
Это начальное и конечное состояние. Оно возникает:- до первого вызова
player:LoadCharacter()(обычно сразу послеPlayerAdded); - после
player:UnloadCharacter()(например, при смерти, телепортации в меню); - при выходе игрока (
PlayerRemoving).
- до первого вызова
-
Создание персонажа
Происходит вызовомplayer:LoadCharacter(), который инициирует:- Создание пустого
Model→ присваиваетсяplayer.Character. - Клонирование базового скелета (R6 или R15, в зависимости от настроек аккаунта):
- R6: 6 частей —
Head,Torso,Left/Right Arm,Left/Right Leg; - R15: 15 частей — добавлены
Upper/LowerTorso,Left/RightHand, суставы.
- R6: 6 частей —
- Установка
PrimaryPart = HumanoidRootPart— точка привязки для перемещения. - Добавление системных объектов:
Humanoid— контроллер здоровья, состояния (Running, Jumping), смерти;Animator— управление анимациями (черезAnimationTrack);HumanoidDescription— описание внешности (тело, лицо, одежда);StarterGear— экипировка по умолчанию (если настроена вStarterPlayer).
- Создание пустого
-
Событие
CharacterAdded
Генерируется после присвоенияplayer.Character = model, но до полной загрузки всех частей. На момент события:model:FindFirstChild("Humanoid")может вернутьnil, если загрузка мешей задержана;Humanoid.Healthчасто равен0в первый тик, затем устанавливается в100;HumanoidRootPartможет отсутствовать на 1–2 кадра (особенно при медленной загрузке ассетов).
-
Полная инициализация
Состояние «персонаж готов» наступает, когда:Humanoid.Health > 0;HumanoidRootPartсуществует и имеетCFrame;Animatorзагрузил базовые анимации (idle,walk,jump).
Рекомендуемый способ ожидания:
player.CharacterAdded:Connect(function(char)
local hrp = char:WaitForChild("HumanoidRootPart", 5)
local hum = char:WaitForChild("Humanoid", 5)
if not (hrp and hum) then return end
hum.HealthChanged:Connect(function(health)
if health > 0 then
-- Персонаж полностью инициализирован
end
end)
end) -
Уничтожение персонажа
Инициируетсяplayer:UnloadCharacter()или автоматически при:Humanoid.Health <= 0(если включено автовоскрешение — создаётся новый);- телепортации в зону без физики;
- выходе из игры.
Перед уничтожением генерируется событиеCharacterRemoving, позволяющее сохранить состояние (позиция, инвентарь в руках).
Объект player (тип Player) содержит:
| Свойство / Дочерний объект | Назначение |
|---|---|
.UserId | Уникальный ID в системе Roblox |
.Name | Отображаемое имя |
.Character | Модель персонажа (м.б. nil при входе/выходе) |
.PlayerGui | GUI, отображаемый только этому игроку |
.Backpack | Инвентарь вне экипировки |
.leaderstats (соглашение) | Папка для счётчиков (уровень, монеты) |
⚠️
player.Characterможет бытьnilне только при входе, но и при смерти, телепортации, смене персонажа.
Обязательные компоненты персонажа
Ниже — минимальный набор объектов, без которых персонаж не будет функционировать как игровая сущность.
-
HumanoidRootPart
ЭтоPart(обычноBallSocketилиBlock), служащий точкой опоры для физики и перемещения. Именно егоCFrameиспользуется движком для:- расчёта позиции игрока в мире;
- привязки камеры;
- определения центра масс в физике.
ЕслиHumanoidRootPartотсутствует илиnil, методы вродеHumanoid:MoveTo()игнорируются.
-
Humanoid
Контроллер состояния персонажа. Его свойства управляют поведением:Health,MaxHealth— система жизни;WalkSpeed,JumpPower— физические параметры;State(Enum.HumanoidStateType) — текущее действие (Walking, Jumping, Dead);SeatPart— если персонаж сидит, ссылается наSeat.
Методы:TakeDamage(),:Sit(),:MoveTo()вызывают изменения состояния и генерируют события (Touched,Died).
-
Animator
Отвечает за проигрывание анимаций. Работает сAnimation(ассеты, загруженные из библиотеки илиAnimationIds).
Важно:Animatorне создаётся вручную — он добавляется автоматически при загрузке персонажа. -
Скелет (R6/R15)
ИерархияPartиJointInstance(BallSocketConstraint,HingeConstraint).
Пример R15:Character
├── HumanoidRootPart
├── LowerTorso
│ ├── LeftLeg
│ └── RightLeg
├── UpperTorso
│ ├── LeftArm
│ └── RightArm
│ └── Head
└── ...Коллизии между частями отключены (
CanCollide = false), чтобы избежать самопроизвольных падений.
Разработчик может добавлять в персонажа:
Tool— экипированный предмет (вBackpackили прямо вCharacter);ProximityPrompt— интерактивные подсказки;Script/LocalScript— поведение, привязанное к персонажу (например,Animate.lua— клонируется изStarterCharacterScripts).
Инвентарь
Термин «инвентарь» в Roblox не имеет единого объекта-контейнера. Вместо этого используется двухуровневая система, разделённая на:
Backpack— серверный контейнер для предметов, не экипированных в текущий момент;PlayerGui+Character— клиентские места отображения (GUI) и экипировки (в руках).
Это разделение отражает архитектурный принцип: сервер управляет состоянием, клиент — отображением.
Backpack — серверное хранилище
-
Это объект типа
Backpack, дочерний по отношению кplayer. -
Создаётся автоматически при подключении игрока, если в
StarterPackесть хотя бы один предмет или явно вызваноplayer:FindFirstChild("Backpack")(ленивая инициализация). -
Содержимое: только объекты типа
Tool(и его наследники, например,HopperBin).
Другие типы (Part,Folder,IntValue) игнорируются движком: они не отображаются в интерфейсе, не подчиняются логике экипировки. -
Жизненный цикл предмета в
Backpack:- Игрок подбирает предмет → сервер вызывает
tool.Parent = player.Backpack. - Предмет появляется в клиентском инвентаре (рендерится через
StarterGui/BackpackFrame). - При нажатии — клиент отправляет
tool:Activate(), сервер получаетtool.Activated. - Сервер проверяет:
—tool:IsDescendantOf(player.Backpack)→ можно экипировать;
—player.Characterсуществует → можно поместить в руки. - Сервер вызывает
tool.Parent = player.Character→ предмет переходит в персонаж.
- Игрок подбирает предмет → сервер вызывает
-
Ограничения:
- Максимум 10 предметов в
Backpackпо умолчанию (настраивается черезStarterPlayer.MaxBackpackItems). - Предметы в
Backpackне участвуют в физике, даже еслиCanCollide = true. Backpackдоступен только на сервере; клиент видит его содержимое через репликацию, но не может напрямую изменять.
- Максимум 10 предметов в
Экипировка — предметы в руках
Когда предмет «в руках», он находится внутри Character, обычно в Torso или UpperTorso.
Типичная иерархия:
Character
└── UpperTorso
└── Sword (Tool)
├── Handle (Part) — видимая модель
└── Script — логика атаки
- Активация: вызов
tool:Activate()на клиенте →tool.Activatedна сервере. - Деактивация:
tool:Deactivate()или смена оружия → сервер перемещаетtool.Parent = player.Backpack.
🔐 Безопасность:
— Клиент не может вызватьtool.Parent = workspaceнапрямую — сервер отклонит операцию.
— Сервер должен проверять, чтоtoolпринадлежит игроку:tool.AncestryChanged:Connect(function(_, newParent)
if newParent ~= player.Character and newParent ~= player.Backpack then
tool.Parent = player.Backpack -- возврат в инвентарь
end
end)
Визуальное отображение (GUI)
Инвентарь не имеет встроенного GUI. Его отображение — обязанность разработчика.
-
Типичный подход:
- В
StarterGuiсоздаётсяScreenGuiсFrameиImageButtonдля каждого слота. - На клиенте
LocalScriptподписывается на:player.Backpack.ChildAdded→ отобразить новый предмет;player.Backpack.ChildRemoved→ убрать иконку;Humanoid.EquipTool/UnequipTool→ подсветить активный слот.
- Для иконок используются
ImageLabelсrbxassetid://...(ассеты из Marketplace или загруженные через DevHub).
- В
-
Важно:
GUI должен обновляться только на клиенте, но данные — браться с сервера черезRemoteEvent. Например:-- Сервер
inventoryUpdate:FireClient(player, { "Sword", "Shield", nil, ... })
-- Клиент
inventoryUpdate.OnClientEvent:Connect(function(slots)
for i, itemName in ipairs(slots) do
slotsGUI[i].Icon.Image = getIconId(itemName)
end
end)
Постоянное хранение
Ни Backpack, ни Character не сохраняются между сессиями. Для долговременного инвентаря используется:
DataStore— сохранение спискаToolпоProductIdили имени:DataStore:SetAsync("Inventory_" .. player.UserId, { "Sword", "Potion" })leaderstats(соглашение) — временные счётчики в рамках сессии:local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 100
coins.Parent = leaderstats
⚠️
leaderstats— это не системный объект, а соглашение сообщества. Его использование не гарантируется, но поддерживается всеми популярными фреймворками (например,StarterPlayerScripts/Chat).
Взаимодействие
Цикл «взять → экипировать → использовать → убрать» выглядит так:
- Сервер создаёт
ToolвWorkspace(например,Sword). - Игрок касается предмета → сервер проверяет расстояние и права →
sword.Parent = player.Backpack. - Клиент получает репликацию → отображает иконку в GUI.
- Игрок нажимает кнопку → клиент вызывает
sword:Activate(). - Сервер получает
sword.Activated→ проверяет:sword.Parent == player.Backpack— можно экипировать;player.Characterсуществует — можно поместить в руки.
- Сервер:
sword.Parent = player.Character.UpperTorso. - Клиент: запускает анимацию, показывает эффекты.
- При использовании (удар) — клиент отправляет
RemoteEvent:FireServer("Attack"), сервер валидирует и применяет урон.
Любой пропущенный шаг (например, отсутствие проверки Parent на сервере) приводит к уязвимости или ошибке.
Транзакции и сервер
Что такое транзакция?
Транзакция — операция, которая должна быть выполнена целиком или не выполнена вовсе.
Пример: «списать 100 монет и выдать меч».
Если списание прошло, а выдача — нет → игрок потерял деньги → недоверие.
Почему сервер — источник истины?
| Операция | Клиентская проверка | Серверная проверка |
|---|---|---|
| Проверка баланса | if balance >= 100 | CurrencyManager:GetBalance(player) >= 100 |
| Списание | balance -= 100 | CurrencyManager:SpendCoins(player, 100) |
| Выдача предмета | inventory:add("Sword") | InventorySystem:UnlockItem(player, "Sword") |
Проблема клиента:
- Любой может открыть DevConsole (F9) и выполнить:
game.Players.LocalPlayer.leaderstats.Coins.Value = 999999 - Или заменить цену в GUI-скрипте.
Сервер защищён: клиент может попросить, но решает сервер.
Как проходит покупка?
Рассмотрим покупку меча за 500 монет:
| Этап | Клиент | Сервер | Платформа |
|---|---|---|---|
| 1. Показ | Отображает кнопку «Купить (500 монет)» | — | — |
| 2. Инициация | BuyEvent:FireServer("Sword", txId) | Получает запрос | — |
| 3. Валидация | — | Проверяет: — txId уникален? — предмет существует? — баланс ≥ 500? | — |
| 4. Исполнение | — | 1. Списывает 500 монет; 2. Добавляет меч в Inventory; 3. Сохраняет в DataStore | — |
| 5. Подтверждение | Получает BuyEvent.OnClientEvent("Success") | — | — |
| 6. Отображение | Обновляет GUI, показывает эффект | — | — |
Если на шаге 4 произошла ошибка → сервер возвращает "Failed" → клиент не меняет баланс (он запрашивает актуальный после ответа).
🔁 При повторной отправке с тем же
txIdсервер отклонит запрос как дубль — это и есть идемпотентность.
Ландшафт
Ландшафт (объект Terrain) — это специализированный компонент среды, предназначенный для создания непрерывных, объёмных, деформируемых поверхностей крупного масштаба: гор, пещер, рек, рельефа местности. В отличие от сборки из отдельных Part, ландшафт представляет собой единое воксельное поле, управляемое движком на уровне низкоуровневых операций. Его существование обусловлено необходимостью баланса между визуальной сложностью, физической достоверностью и производительностью при масштабах, недостижимых через классический подход с BasePart.
Внутреннее устройство
Ландшафт организован как трёхмерная регулярная сетка ячеек — вокселей (volume pixels). Каждый воксель имеет фиксированный размер — 2 × 2 × 2 метра (это значение неизменно и не настраивается). Вся доступная для редактирования область делится на чанки размером 64 × 64 × 64 вокселя (то есть 128 × 128 × 128 метров в мире). Чанки загружаются и выгружаются динамически по мере перемещения игрока, что позволяет работать с картами размером до 8192 × 8192 × 512 метров (теоретический лимит), не перегружая память.
Каждый воксель хранит несколько атрибутов:
- Материал — определяет визуальный вид и физические свойства (земля, камень, вода, песок и др.; всего 36 встроенных материалов).
- Форма — геометрия внутри вокселя: пусто, полный куб, наклонная плоскость (ramp), угловая поверхность (wedge) и комбинации (например, «четверть куба»). Это позволяет создавать плавные склоны без ступенчатых переходов.
- Цвет — корректирующий оттенок (tint), накладываемый на базовую текстуру материала.
Движок генерирует единый меш на каждый чанк, объединяя все воксели с одинаковым материалом и смежными гранями. Этот процесс называется mesh stitching. Благодаря ему количество отрисовываемых объектов остаётся пропорциональным числу чанков, а не числу вокселей — что даёт решающее преимущество в производительности при больших ландшафтах.
Физическое поведение
Физика ландшафта реализована на основе воксельной коллизионной сетки, а не полигональных мешей. Это означает, что коллизии рассчитываются не по точной геометрии поверхности, а по упрощённой сетке, где каждый воксель представляет собой куб или призму. Точность коллизий зависит от разрешения формы внутри вокселя, но даже в максимальном качестве она уступает MeshPart с высокополигональной коллизией.
Ключевые особенности:
- Объекты с
CanCollide = trueвзаимодействуют с ландшафтом как с твёрдой поверхностью;Anchored = falseчасти падают и останавливаются на ней. TouchedиTouchEndedработают, но с задержкой и возможными «дрожаниями» при движении вдоль наклонных поверхностей (из-за дискретности вокселей).- Физические свойства (трение, упругость) наследуются от материала:
Material.Grassимеет низкое трение,Material.Ice— ещё ниже,Material.Sand— высокое сопротивление. - Вода (
Material.Water) — не физическая жидкость, а визуальный эффект с ограниченной глубиной (максимум 20 метров). Объекты под водой получают замедление черезHumanoid.WalkSpeed, но потоков, течений или плавучести нет.
Важно: ландшафт не участвует в динамической физике — его нельзя толкнуть, разрушить или переместить физическим воздействием. Любые изменения должны инициироваться программно или через инструменты редактора.
Редактирование
В Studio ландшафт редактируется через панель Terrain, доступную при выделении объекта Terrain в Workspace. Основные инструменты:
- Raise/Lower — изменение высоты поверхности;
- Smooth — сглаживание рельефа;
- Paint — смена материала;
- Fill — заливка объёма (например, создание куба земли);
- Erase — удаление вокселей (создание пустоты, пещер);
- Region Paste — копирование фрагментов между местами или играми.
Программное управление осуществляется через API объекта Terrain, доступного как workspace.Terrain. Ключевые методы:
-
:FillBlock(region: Region3, material: Enum.Material)
Заполняет указанный куб материалом.Region3должен быть выровнен по воксельной сетке (координаты кратны 2). -
:FillWedge(region: Region3, material: Enum.Material)
Заполняет клин (полезно для склонов). -
:ReadVoxels(region: Region3, resolution: number)
Возвращает трёхмерный массив данных вокселей в заданной области.resolution= 4 — стандартное разрешение (1 воксель = 1 элемент массива). -
:WriteVoxels(region: Region3, resolution: number, data: VoxelData)
Записывает данные в ландшафт. Используется для процедурной генерации (например, шум Перлина → высота → материал). -
:PlaceAsync(region: Region3, instances: {Instance})
Встраивает стандартныеPart,MeshPartв ландшафт (например, камни на поверхности), сохраняя их физику и скрипты.
Ограничения API:
- Все операции синхронные и блокирующие — при работе с большими областями (> 1 млн вокселей) возможны фризы.
- Максимальная область в одном вызове — 2048 × 2048 × 256 вокселей.
- Изменения мгновенно реплицируются всем клиентам (в отличие от
Part, где можно скрыть промежуточные состояния).
Производительность и масштабируемость
Ландшафт оптимизирован для статических или медленно меняющихся сцен. Его преимущества проявляются при площади поверхности свыше 500 × 500 метров:
- 1 чанк = 1 draw call для рендера, 1 физический объект для движка;
- в то время как сборка из
Partтого же размера потребовала бы тысяч draw call’ов и объектов физики.
Однако у ландшафта есть узкие места:
- Изменение в реальном времени (например, копание тоннелей игроками) требует частых вызовов
:FillBlock(), что создаёт нагрузку на CPU и сеть (из-за репликации). - LOD (Level of Detail) для ландшафта отсутствует — чанки всегда рендерятся в полном разрешении.
- Ограничения на материалы: нельзя использовать кастомные текстуры напрямую — только через
Terrain:DecorateAsync()с предварительно загруженными ассетами.
Для гибридных решений рекомендуется:
- использовать
Terrainдля базового рельефа (горы, реки); - добавлять детализацию через
Part/MeshPart(камни, деревья, здания); - избегать частого изменения вокселей — кэшировать операции, применять изменения пакетно.
Совместимость и ограничения платформы
- Ландшафт поддерживается на всех платформах, включая мобильные устройства и консоли.
- В мобильных сборках используется упрощённый шейдер рендера — детали текстур и тени могут быть снижены.
- Максимальная высота по оси Y — 500 метров (вокселей выше
y = 250не существует); максимальная глубина — −200 метров. - Невозможно создать «плавающие» острова без опоры — воксели в воздухе удаляются автоматически, если не поддерживаются снизу (настройка
workspace.Terrain.AutoStretchFill).
Когда использовать ландшафт, а когда — Part
Выбор обусловлен задачей, а не предпочтениями:
-
Используйте
Terrain, если:- требуется непрерывный рельеф (горы, долины, пещеры);
- карта превышает 300 × 300 метров;
- важна производительность на слабых устройствах;
- допустимы приблизительные коллизии.
-
Используйте
Part/MeshPart, если:- нужна точная геометрия (архитектура, интерьеры);
- важны детальные коллизии (платформер, паркур);
- объекты должны быть динамическими (движущиеся платформы, разрушаемые стены);
- требуется кастомная текстура на отдельном элементе.
Гибридный подход (ландшафт + декор из Part) — стандартная практика в профессиональных проектах.
Добавление, удаление, изменение объектов
Через интерфейс (Studio)
- Добавить:
Home → Part/Model/Script. - Удалить: выделить → Delete / ПКМ → Delete.
- Изменить свойство: выделить →
Properties→ ввести значение.
Через код
| Действие | Код |
|---|---|
| Создать | local p = Instance.new("Part") |
| Разместить | p.Parent = workspace |
| Изменить свойство | p.Size = Vector3.new(5, 1, 5) |
| Найти по имени | workspace:FindFirstChild("Ground") |
| Найти по пути | game.Players.Player1.PlayerGui.ScoreLabel |
| Удалить | p:Destroy() |
✅
:Destroy()предпочтительнееp.Parent = nil, потому что:
— отключает события;
— освобождает память;
— не оставляет «висячих» ссылок.
Монетизация
Что такое Robux?
- Валюта платформы Roblox.
- Приобретается за реальные деньги.
- Разработчик не управляет балансом Robux игрока — только запрашивает покупку.
Как работает покупка за Robux?
⚠️ Запрещено:
- Запрашивать платёж из
LocalScriptбез подтверждения черезPromptPurchase.- Выдавать предмет до получения
PromptPurchaseFinished.- Использовать
RemoteEventдля имитации покупки за Robux.
Архитектура проекта в Roblox Studio
Структура сцены
Каждый проект в Roblox Studio строится вокруг понятия места (Place) — файла, содержащего всю информацию об игровом мире: объекты, скрипты, настройки, ресурсы. Проект хранится в виде .rbxl или .rbxlx, что позволяет сохранять состояние сцены между сессиями редактирования.
Центральным элементом редактора является окно Explorer, отображающее иерархию всех объектов в текущем месте. Объекты организованы в древовидную структуру, где каждый узел может содержать дочерние элементы. Это позволяет группировать связанные компоненты, например, помещая все элементы игровой среды в папку Game Environment.
Пример:
Workspace
├── Game Environment
│ ├── Ground
│ ├── Obstacle
│ └── Platforms
├── StarterGui
└── ReplicatedStorage
Такая организация способствует поддержке чистоты кода и упрощает навигацию по проекту.
Основные контейнеры
- Workspace — корневой контейнер для всех видимых объектов в игровом мире: персонажи, платформы, препятствия.
- StarterGui — содержит элементы графического интерфейса (GUI), которые автоматически клонируются для каждого игрока при подключении.
- ReplicatedStorage — используется для хранения данных и объектов, доступных как на сервере, так и на клиенте.
- ServerScriptService — место для размещения серверных скриптов, управляющих логикой игры.
- Lighting — хранит параметры освещения, времени суток, тумана.
- Players — содержит данные о каждом игроке, включая его персонажа (
Character) и инвентарь.
Правильное использование этих контейнеров обеспечивает предсказуемое поведение объектов и корректную работу системы репликации.
Интерфейс Roblox Studio
Основные панели
Интерфейс Roblox Studio состоит из нескольких ключевых компонентов:
- Главная область (Viewport) — трёхмерное пространство, в котором происходит редактирование сцены.
- Панель инструментов (Toolbar) — расположена сверху, содержит инструменты для выбора, перемещения, изменения размеров объектов, а также запуска и остановки игры.
- Explorer — дерево объектов сцены, позволяющее добавлять, удалять и переупорядочивать элементы.
- Properties — отображает и позволяет изменять свойства выбранного объекта (размер, положение, цвет, прозрачность, коллизии и др.).
- Toolbox — библиотека готовых моделей, эффектов, скриптов и материалов.
- Output — консоль, выводящая сообщения от скриптов, ошибки, предупреждения и результаты выполнения команд.
Доступ к этим панелям осуществляется через меню View. Например, чтобы открыть консоль, необходимо выбрать View → Output.
Управление камерой
Управление вьюпортом аналогично другим 3D-редакторам:
- W, A, S, D — перемещение камеры.
- Колесо мыши — масштабирование.
- ПКМ + движение мыши — вращение камеры.
- Зажатие Q — орбитальное вращение вокруг центра.
Эти действия позволяют эффективно навигировать по сцене и точно размещать объекты.
Создание игровой среды
Базовые объекты: Part и BasePart
Все физические объекты в Roblox основаны на классе BasePart. Наиболее часто используется Part — параллелепипед, который можно масштабировать, вращать и раскрашивать.
Пример: создание земли
- В Explorer создайте папку
Game Environment. - Внутри неё добавьте новый
Partи назовите егоGround. - Используйте инструмент Resize для изменения размеров (например, 100×1×100).
- В Properties установите материал:
GrassилиConcrete. - При необходимости измените цвет через свойство
Color.
Работа с ландшафтом
Для создания естественного рельефа используется встроенный Terrain Editor.
Шаги:
- Перейдите в меню
View → Terrain. - Выберите инструмент:
- Raise/Lower — изменение высоты поверхности.
- Smooth — сглаживание переходов.
- Paint — нанесение текстур (трава, песок, камень).
- Для добавления воды используйте режим Water, задав уровень и форму водоёма.
Ландшафт поддерживает многослойную текстуризацию и может быть экспортирован/импортирован для повторного использования.
Программирование на Lua в Roblox
Основы Lua
Roblox использует диалект Lua 5.1 с расширениями API, специфичными для платформы. Язык интерпретируется в реальном времени, что позволяет быстро тестировать изменения.
Типичный скрипт в Roblox:
local part = script.Parent
part.Touched:Connect(function(hit)
print("Объект задет: " .. hit.Name)
end)
Типы скриптов
- Script — выполняется на сервере. Используется для логики, требующей надёжности (например, проверка победы).
- LocalScript — выполняется на клиенте. Подходит для GUI, анимаций, ввода с клавиатуры.
- ModuleScript — содержит переиспользуемый код, импортируется через
require().
Обработка событий
События — основа реактивной архитектуры в Roblox. Большинство объектов имеют встроенные события:
-- Обработка клика по кнопке
script.Parent.MouseButton1Click:Connect(function(player)
print(player.Name .. " нажал кнопку")
end)
-- Изменение положения персонажа
game.Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(char)
char:WaitForChild("HumanoidRootPart").Touched:Connect(function(hit)
-- Логика столкновения
end)
end)
end)
Асинхронность и задержки
Для отложенного выполнения используется task.wait() (аналог delay()):
task.spawn(function()
task.wait(5)
print("Прошло 5 секунд")
end)
Физика и коллизии
Система физики
Roblox включает встроенную физическую модель, основанную на двигателе NVIDIA PhysX. Все BasePart по умолчанию участвуют в симуляции, если не отключено свойство Anchored.
Ключевые свойства:
Anchored— фиксирует объект в пространстве.CanCollide— определяет, будет ли объект сталкиваться с другими.Massless— игнорирует влияние массы при столкновениях.Gravity— включение/выключение гравитации для части.
PhysicsService
Для точного управления взаимодействием между объектами используется PhysicsService, позволяющий назначать объекты в разные группы коллизий:
local PhysicsService = game:GetService("PhysicsService")
PhysicsService:CreateCollisionGroup("Player")
PhysicsService:CollisionGroupSetCollidable("Player", "Obstacle", false)
Графический интерфейс (GUI)
Элементы интерфейса
GUI создаётся с помощью объектов внутри StarterGui. Основные типы:
ScreenGui— контейнер для 2D-интерфейса.Frame— прямоугольная область.TextButton,ImageButton— кнопки.TextLabel,TextBox— отображение и ввод текста.
Пример: кнопка старта
local screenGui = Instance.new("ScreenGui")
local button = Instance.new("TextButton")
button.Size = UDim2.new(0, 150, 0, 50)
button.Position = UDim2.new(0.5, -75, 0.5, -25)
button.Text = "Start Game"
button.Parent = screenGui
button.MouseButton1Click:Connect(function()
print("Игра начата!")
end)
screenGui.Parent = game.StarterGui
Координатная система GUI
Используется относительная система координат через UDim2, где:
Scale— часть от размера родителя (0–1).Offset— фиксированный сдвиг в пикселях.
Система уровней и прогресса
Реализация уровней
Система уровней может быть построена на основе накопления опыта (XP). При достижении порога — повышение уровня.
local playerData = {
Level = 1,
XP = 0,
XPToNextLevel = 100
}
function addXP(amount)
playerData.XP = playerData.XP + amount
while playerData.XP >= playerData.XPToNextLevel do
playerData.XP = playerData.XP - playerData.XPToNextLevel
playerData.Level = playerData.Level + 1
playerData.XPToNextLevel = math.floor(playerData.XPToNextLevel * 1.5)
print("Уровень повышен до: " .. playerData.Level)
end
end
Хранение данных
Для сохранения прогресса используется DataStore:
local DataStoreService = game:GetService("DataStoreService")
local playerStore = DataStoreService:GetDataStore("PlayerData")
-- Сохранение
playerStore:SetAsync("Player_"..player.UserId, playerData)
-- Загрузка
local data = playerStore:GetAsync("Player_"..player.UserId)
Важно: DataStore имеет ограничения на частоту вызовов и требует обработки ошибок.
Мультиплеер и репликация
Сервер-клиент архитектура
Roblox использует авторитетный сервер:
- Все критические решения принимаются на сервере.
- Клиенты получают только визуальные и интерфейсные обновления.
RemoteEvents и RemoteFunctions
Для передачи данных между клиентом и сервером используются:
RemoteEvent— односторонняя отправка (например, клик по кнопке).RemoteFunction— запрос с ответом (например, получение данных игрока).
Пример:
ServerScript
local remoteEvent = game.ReplicatedStorage.RemoteEvent
remoteEvent.OnServerEvent:Connect(function(player, action)
if action == "Jump" then
-- Выполнить действие на сервере
end
end)
LocalScript
remoteEvent:FireServer("Jump")
Анимации и моделирование
Создание анимаций
Анимации создаются в Animation Editor:
- Выделите персонажа или часть.
- Откройте
Window → Animation Editor. - Добавьте ключевые кадры для свойств (позиция, вращение).
- Экспортируйте анимацию в
AnimationController.
Скриптовое управление анимациями
local animator = character:WaitForChild("Animator")
local animation = Instance.new("Animation")
animation.AnimationId = "rbxassetid://123456789"
local track = animator:LoadAnimation(animation)
track:Play()
Тестирование и отладка
Консоль вывода (Output)
Окно Output показывает:
- Результаты
print(). - Ошибки выполнения скриптов.
- Предупреждения о производительности.
Инструменты отладки
Debug → Stats— мониторинг FPS, памяти, количества частей.Test → Play— запуск локальной сессии.TeleportService— тестирование перехода между местами.
Публикация проекта
Подготовка к публикации
- Убедитесь, что все скрипты протестированы.
- Проверьте права доступа к ассетам.
- Настройте настройки места: название, описание, теги.
- Установите уровень приватности (публичный, друзья, приватный).
Процесс публикации
- Нажмите Publish to Roblox.
- Выберите существующее место или создайте новое.
- Загрузите изменения.
- Получите ссылку для распространения.
После публикации игра становится доступна другим пользователям. Возможна интеграция с системой monetization (продажа предметов за Robux).
Методология разработки
Итеративный подход
- Прототипирование — создание минимального воспроизводимого примера (MVP).
- Тестирование — проверка на разных устройствах и соединениях.
- Оптимизация — снижение количества частей, использование
MeshPartвместо сложных конструкций. - Сбор обратной связи — анализ действий игроков, исправление багов.
Рекомендации
- Начинайте с простых проектов (платформеры, мини-игры).
- Используйте готовые ресурсы из Toolbox.
- Документируйте код и структуру проекта.
- Регулярно сохраняйте и коммитьте изменения (при использовании внешних систем контроля версий).
Система магазина и монет в Roblox Studio
Внутриигровая экономика — один из ключевых элементов современных игровых проектов, особенно в многопользовательских и сервисных играх, ориентированных на длительное вовлечение. Roblox, как платформа, предоставляет разработчикам инструменты для построения собственных экономических систем, включая магазины, валюту, транзакции и интеграцию с платёжными средствами. Однако, несмотря на высокий уровень абстракции, которую предлагает движок, понимание принципов построения таких систем требует системного подхода: от проектирования логики до обеспечения безопасности и масштабируемости.
Настоящая глава посвящена реализации магазина в Roblox Studio, и архитектурному осмыслению того, что представляет собой внутриигровой магазин как подсистема, как он интегрируется в общую структуру проекта, и какие ограничения и возможности накладывает платформа Roblox на разработчика. Мы последовательно разберём концепции, их взаимосвязи и реализацию, при этом сохраняя акцент на устойчивости, читаемости кода и перспективе дальнейшего развития проекта.
1. Что такое внутриигровой магазин
Внутриигровой магазин — это программная подсистема, которая реализует функции выбора, приобретения и выдачи игровых активов (предметов, улучшений, скинов, способностей и т.д.) в обмен на одну или несколько форм внутриигровой валюты. Магазин — это целостный сервис, включающий:
- Каталог товаров — структура данных, описывающая доступные предметы, их стоимость, условия приобретения и эффекты после покупки;
- Систему валюты — механизм учёта баланса игрока, валидации транзакций и обновления данных;
- Механизмы выдачи и применения — логика, которая реагирует на успешную транзакцию и производит соответствующие изменения в состоянии игры (например, надевает скин, открывает уровень, выдаёт инвентарь);
- Интерфейс взаимодействия — визуальный и поведенческий слой, через который игрок взаимодействует с магазином;
- Систему сохранения и синхронизации — способ гарантированного сохранения состояния покупок между сессиями и при переходе между серверами.
Важнейшее свойство хорошего магазина — идемпотентность операций. Это означает, что повторное выполнение одной и той же операции (например, повторная отправка запроса на покупку) не приводит к нежелательным последствиям, таким как многократное списание валюты или дублирование предмета. В распределённых системах, какими являются игры на Roblox, такая устойчивость к сетевым артефактам критична.
Подсистема магазина должна быть изолирована от основной игровой логики. Это достигается за счёт чёткого разделения ответственности: модуль магазина не должен напрямую менять состояние персонажа или мира, а только отправлять события или вызывать строго определённые методы других систем (например, через шину событий или менеджер инвентаря). Такой подход обеспечивает тестируемость, модульность и упрощает дальнейшее расширение функционала.
2. Что такое магазин и монеты в Roblox: платформенные особенности
Roblox предоставляет две основные формы валюты: Robux — официальная платёжная валюта платформы, приобретаемая за реальные деньги, и внутриигровые валюты — пользовательские единицы, создаваемые разработчиком (например, «золото», «кристаллы», «очки опыта»). Эти два типа валют принципиально различаются по происхождению, способу управления и правовым последствиям.
2.1. Robux и Developer Products
Robux — это валюта, принадлежащая платформе. Разработчик не может напрямую управлять балансом Robux игрока. Вместо этого используется механизм Developer Products — зарегистрированных в системе DevHub цифровых товаров, привязанных к конкретной игре. При покупке такого товара Roblox обрабатывает платёж, списывает Robux с аккаунта игрока и отправляет подтверждённое событие в игру через MarketplaceService:PromptPurchase() и последующий MarketplaceService.PromptPurchaseFinished.
Ключевые особенности:
- Все транзакции с Robux проходят через серверы Roblox — клиентская сторона не может инициировать списание напрямую.
- Разработчик получает уведомление о покупке асинхронно, и только после получения подтверждения от сервера может выдать предмет.
- Поддерживается механизм возврата (refund) — в течение определённого времени игрок может отменить покупку, и разработчик обязан корректно обработать отмену (например, отозвать предмет).
- Developer Products неизменяемы после публикации: нельзя изменить цену или название без создания нового продукта. Это требует тщательного планирования при запуске проекта.
Таким образом, магазин, использующий Robux, всегда является гибридной системой: клиент отображает интерфейс, сервер Roblox обрабатывает платёж, а сервер игры (Game Server) — выдаёт награду.
2.2. Внутриигровые валюты
Любая валюта, не являющаяся Robux (например, «монеты», «жетоны»), полностью управляется разработчиком. Её баланс хранится в DataStore, в плейерских атрибутах (Player:SetAttribute / GetAttribute) или в пользовательских объектах (например, Folder в PlayerGui или Backpack), но только в рамках сессии — для постоянного хранения обязателен DataStore.
Особенности:
- Разработчик сам определяет правила начисления (за уровень, за задание, за время) и расходования.
- Отсутствует встроенная защита от мошенничества: клиентская часть может быть взломана, если логика проверки баланса реализована только на клиенте.
- Нет автоматической интеграции с платёжными системами: конвертация реальных денег в такую валюту возможна только через Developer Product («купить 1000 монет за 50 Robux»).
Важно подчеркнуть: никакая внутриигровая валюта не может быть обменена на Robux игроком — это нарушает условия использования платформы. Обратный обмен (Robux → внутриигровая валюта) разрешён и является стандартной практикой.
3. Монетизация в Roblox и как разработчики получают доход
Монетизация в Roblox строится вокруг нескольких официальных каналов, каждый из которых требует соблюдения политик и технических требований платформы.
3.1. Developer Products (одноразовые покупки)
Это основной способ продажи цифровых товаров: скины, улучшения, внутриигровая валюта, косметика. Разработчик создаёт продукт в DevHub, задаёт цену в Robux, привязывает его к игре и обрабатывает событие покупки в коде. Roblox берёт комиссию ~30% (в зависимости от условий партнёрской программы и типа аккаунта — individual vs group).
3.2. Game Passes (игровые пропуска)
Game Pass — это разовая покупка, дающая постоянное преимущество: доступ к эксклюзивному контенту, бонусам, привилегиям (например, двойной опыт, уникальный персонаж). Game Pass привязан к аккаунту игрока и действует во всех сессиях игры. Технически проверка наличия Game Pass осуществляется через GamePassService:UserOwnsGamePassAsync().
Преимущество Game Pass перед Developer Product — в постоянстве эффекта и простоте проверки. Недостаток — невозможность динамического изменения функционала после выпуска (за исключением программной логики, активируемой наличием пропуска).
3.3. Premium Payouts (доход от подписки Roblox Premium)
Разработчики получают долю от времени, проведённого подписчиками Premium в их игре, через систему Engagement-Based Payouts. Это пассивный доход, не требующий реализации магазина, но зависящий от удержания аудитории. Расчёт производится ежемесячно и зависит от доли активного времени подписчиков в конкретной игре относительно всего экосистемного времени.
3.4. Виртуальные товары и пользовательский контент (UGC)
С 2023 года Roblox активно развивает систему UGC (User-Generated Content) — пользователи могут создавать и продавать свои ассеты (одежда, аксессуары, анимации), а разработчики — интегрировать их в игру через официальные API. Это создаёт дополнительные возможности для монетизации через комиссии и кураторство.
Важное ограничение: все платёжные операции должны проходить через официальные сервисы Roblox. Попытки организовать сторонние платежи (например, через Telegram-бота или внешний сайт) ведут к бану игры и аккаунта.
4. Как создать свою систему магазина и монет в Roblox Studio
Построение собственной экономической системы в Roblox требует технической реализации и проектирования архитектуры, устойчивой к ошибкам, мошенничеству и изменениям в требованиях. Ниже рассмотрены ключевые компоненты такой системы — от концептуальной модели до кодовой структуры. Мы будем избегать «быстрых решений» вроде скриптов в StarterGui, ориентируясь на промышленные практики разработки: разделение ответственности, защита от клиентских атак, восстанавливаемость состояния и поддерживаемость.
4.1. Архитектурные принципы
Любая устойчивая система магазина в Roblox должна соответствовать следующим принципам:
-
Сервер — единственный источник истины.
Вся логика, связанная с проверкой баланса, списанием валюты и выдачей предметов, должна выполняться на сервере (ServerScriptServiceилиReplicatedStorage). Клиент (StarterGui,LocalScript) может инициировать запрос, но не принимать решение о допустимости операции. -
Полная изоляция данных.
Данные об инвентаре, валюте и истории покупок хранятся вDataStore, а не вPlayerGuiилиBackpack. Временные копии могут быть на клиенте для отображения, но они всегда должны синхронизироваться с сервером и считаться недостоверными до подтверждения. -
Событийная модель взаимодействия.
Клиент отправляет запросы. Сервер обрабатывает их, валидирует, и при успехе отправляет подтверждение или отказ. Это предотвращает race-conditions и обеспечивает предсказуемость. -
Идемпотентность транзакций.
Каждая покупка должна иметь уникальный идентификатор (например,transactionId = os.time() .. "_" .. playerId). При повторной отправке запроса с тем же ID сервер не должен выполнять операцию дважды. Это защищает от сетевых дублей и намеренных атак. -
Отказоустойчивость при сохранении.
Операция «списать валюту и выдать предмет» должна быть атомарной: либо оба действия выполнены, либо ни одно. Для этого применяется паттерн двухфазного сохранения:- Сервер временно блокирует баланс игрока в памяти.
- Выполняет выдачу предмета (например, добавляет в
PlayerData.Inventory). - Выполняет сохранение в
DataStore. - При успехе — фиксирует списание; при ошибке — откатывает изменения.
4.2. Проектирование структуры данных
Перед написанием кода необходимо определить, как будут храниться и передаваться данные. Это влияет на масштабируемость и удобство отладки.
4.2.1. Структура каталога товаров
Каталог товаров — это статическая или полу-статическая конфигурация. Лучше всего её хранить в ReplicatedStorage в виде ModuleScript, например:
-- ReplicatedStorage/Catalog/Items.lua
return {
Sword_001 = {
Id = "Sword_001",
DisplayName = "Огненный клинок",
Description = "Меч, оставляющий след из пламени",
Price = { Coins = 500 },
Type = "Tool",
Unlockable = true,
Metadata = {
Damage = 25,
FireEffect = true
}
},
Skin_Warrior_Red = {
Id = "Skin_Warrior_Red",
DisplayName = "Красный воин",
Description = "Эксклюзивный скин для класса Воин",
Price = { RobuxProduct = "prod_123abc" }, -- ссылка на Developer Product ID
Type = "Appearance",
Metadata = {
MeshId = "rbxassetid://123456789",
TextureId = "rbxassetid://987654321"
}
}
}
Обратите внимание:
- Цена может быть указана как в пользовательской валюте (
Coins), так и через ссылку наDeveloperProduct(по ID или имени). - Все метаданные предмета — строго структурированы. Это позволяет системе выдачи интерпретировать их без жёсткой привязки к конкретным скриптам.
- Каталог не содержит информации о наличии у игрока — только описание товара.
4.2.2. Структура данных игрока
Данные игрока (PlayerData) хранятся в DataStore и должны включать:
{
Coins = 1250,
Inventory = {
["Sword_001"] = { Count = 1, Equipped = true },
["Skin_Warrior_Red"] = { Unlocked = true }
},
PurchaseHistory = {
{ TransactionId = "1731156480_12345", ItemId = "Sword_001", Timestamp = 1731156480 },
{ TransactionId = "1731156800_12345", ItemId = "prod_123abc", Timestamp = 1731156800 }
}
}
Элементы:
Coins— баланс пользовательской валюты.Inventory— карта предметов. Каждый предмет описывается минимально: достаточно флагов (Unlocked,Equipped,Count), а не полной копии каталога.PurchaseHistory— журнал транзакций для аудита, отладки и реализации идемпотентности.
Важно: никогда не храните пароли, токены или приватные ключи в DataStore. Все данные, сохраняемые через DataStoreService, шифруются Roblox, но не предназначены для хранения секретов.
4.3. Реализация клиент-серверного взаимодействия
Взаимодействие между клиентом и сервером строится на RemoteEvent и RemoteFunction. Используйте два отдельных канала:
RemoteEvent— для асинхронных действий (покупка, запрос обновления баланса).RemoteFunction— для синхронных запросов («можно ли купить?», «получить текущий баланс?»).
Пример: запрос на покупку
-
Клиент (
LocalScriptв GUI):local BuyEvent = game:GetService("ReplicatedStorage"):WaitForChild("RemoteEvents"):WaitForChild("BuyItem")
BuyEvent:FireServer("Sword_001", "tx_" .. tick()) -
Сервер (
ScriptвServerScriptService):local BuyEvent = script.Parent:WaitForChild("BuyItem")
BuyEvent.OnServerEvent:Connect(function(player, itemId, transactionId)
if not player or not itemId or not transactionId then return end
-- Проверка дубликата транзакции
if isTransactionProcessed(player.UserId, transactionId) then
warn("Duplicate transaction:", transactionId)
return
end
local catalog = require(game.ReplicatedStorage.Catalog.Items)
local item = catalog[itemId]
if not item then return end
local playerData = getPlayerData(player) -- загрузка из DataStore или кэша
if not playerData then return end
-- Валидация: достаточно ли валюты?
local cost = item.Price.Coins
if cost and playerData.Coins < cost then
-- Отправить отказ клиенту
fireClientEvent(player, "PurchaseFailed", { Reason = "InsufficientFunds" })
return
end
-- Атомарное выполнение
if commitPurchase(player, playerData, item, transactionId) then
fireClientEvent(player, "PurchaseSuccess", { ItemId = itemId })
else
fireClientEvent(player, "PurchaseFailed", { Reason = "ServerError" })
end
end)
Ключевые моменты:
isTransactionProcessedпроверяетPurchaseHistory.commitPurchaseвыполняет: списание валюты, добавление в инвентарь, сохранение в DataStore, запись в историю — и только при полном успехе возвращаетtrue.- Клиент получает только событие — без деталей баланса (во избежание утечки информации).
4.4. Интеграция с Robux (Developer Products)
Для продажи через Robux используется MarketplaceService. Важно: никогда не доверяйте клиенту информацию о покупке.
Правильная последовательность:
- Клиент вызывает
MarketplaceService:PromptPurchase(player, productId). - Игрок подтверждает покупку (всплывающее окно от Roblox).
- Roblox отправляет событие
PromptPurchaseFinishedна сервер. - Сервер проверяет:
- Принадлежит ли
productIdожидаемому товару? - Не была ли покупка уже обработана?
- Корректен ли
player?
- Принадлежит ли
- При успехе — выдаёт предмет, сохраняет.
Пример обработчика на сервере:
local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
MarketplaceService.PromptPurchaseFinished:Connect(function(player, productId, purchased)
if not purchased then return end
local productMap = {
[123456789] = "Coins_1000", -- ID продукта → внутренний ID товара
[987654321] = "Skin_Warrior_Red"
}
local internalId = productMap[productId]
if not internalId then
warn("Unknown product ID:", productId)
return
end
local transactionId = "robux_" .. productId .. "_" .. os.time()
if isTransactionProcessed(player.UserId, transactionId) then return end
local playerData = getPlayerData(player)
local catalog = require(game.ReplicatedStorage.Catalog.Items)
local item = catalog[internalId]
if item and commitPurchase(player, playerData, item, transactionId) then
-- Успешно
else
-- Логирование ошибки — без уведомления игрока (во избежание спама)
end
end)
Замечание: Roblox гарантирует, что PromptPurchaseFinished срабатывает только после подтверждения платёжа на сервере Roblox. Это делает его безопасным.
4.5. Безопасность: типичные уязвимости и защита
4.5.1. Подделка запросов на клиенте
Если логика «хватает предмет из каталога и вычитает цену» реализована в LocalScript, злоумышленник может изменить цену на 0 и купить всё.
Защита: Вся валидация — на сервере. Клиент отправляет только itemId, сервер сам смотрит цену в каталоге.
4.5.2. Race condition при одновременных покупках
Два запроса на покупку одного и того же предмета (например, последнего в лимитированной серии) могут пройти, если проверка баланса и списание не атомарны.
Защита: Блокировка игрока на время транзакции (например, через coroutine или флаг playerData.Locked), либо использование DataStore:UpdateAsync, который гарантирует сериализацию.
4.5.3. Потеря данных при ошибке сохранения
Если после списания валюты произошла ошибка сохранения, игрок потеряет деньги.
Защита: Паттерн «сначала сохранить, потом применить»:
- Создать копию
playerData. - Внести изменения в копию.
- Попытаться сохранить копию через
UpdateAsync. - При успехе — применить изменения в текущее состояние игрока.
4.5.4. Спам-атаки
Злоумышленник может отправлять тысячи запросов на покупку в секунду.
Защита: Rate-limiting на уровне сервера — например, разрешать не более 3 запросов в секунду на игрока, с использованием Debounce.
4.6. Тестирование и отладка
- Используйте
TestServiceдля эмуляции покупок в Studio без траты Robux. - Создайте
Debug-режим: приgame.PlaceId == 0(локальный запуск) разрешать выдачу валюты по нажатию клавиши. - Логируйте все транзакции в
print()или черезHttpServiceв внешний сервис (например, Discord-вебхук для разработчиков). - Пишите unit-тесты для
commitPurchase,isTransactionProcessedи других критичных функций — с использованием mock-объектов.
4.7. Расширяемость и поддержка
Хорошая система магазина должна позволять:
- Добавлять новые валюты без изменения ядра.
- Подключать модули выдачи (например,
ToolSystem,AppearanceSystem) через интерфейсы. - Поддерживать A/B-тестирование цен (через конфигурационный
ModuleScript, переключаемый по флагу).
Рекомендуется выделить следующие модули:
CurrencyManager— управление балансами.InventorySystem— хранение и синхронизация инвентаря.TransactionProcessor— валидация и выполнение покупок.CatalogService— загрузка и кэширование каталога.DataStoreAdapter— абстракция надDataStoreService.
Это позволяет заменять компоненты (например, перейти с GlobalDataStore на OrderedDataStore для лидербордов) без переписывания всей логики.
🛠️ Гайд: Создание собственной системы магазина и монет в Roblox Studio
Цель: построить систему, в которой игрок может:
- зарабатывать внутриигровые монеты (Coins);
- тратить их на предметы в магазине;
- покупать монеты за Robux через Developer Product;
- сохранять прогресс между сессиями;
- быть защищён от подделки транзакций.
Ограничения:
- Никакой логики на клиенте, кроме отображения и отправки запросов.
- Все данные — через
DataStore.- Поддержка идемпотентности и предотвращение race condition.
Шаг 1. Подготовка проекта: структура
Создайте следующую иерархию в Explorer:
ReplicatedStorage/
├── Catalog/
│ └── Items.lua -- каталог товаров (ModuleScript)
├── Services/
│ ├── CurrencyManager.lua
│ ├── InventorySystem.lua
│ ├── TransactionProcessor.lua
│ └── DataStoreAdapter.lua
├── RemoteEvents/
│ ├── BuyItem.lua -- RemoteEvent
│ └── RequestBalance.lua -- RemoteEvent
ServerScriptService/
├── MainEconomySystem.lua -- точка входа
StarterGui/
└── ShopGUI/
└── ShopFrame.lua -- ScreenGui с LocalScript внутри
💡 Почему так?
Разделение по папкам обеспечивает читаемость и предотвращает "скриптовый хаос". Серверные сервисы — вReplicatedStorage/Services, клиентская логика — вStarterGui, события — отдельно. Это соответствует best practices Roblox и позволяет легко находить компоненты.
Шаг 2. Создание каталога товаров
ReplicatedStorage/Catalog/Items.lua (ModuleScript)
-- Описание всех предметов в магазине.
-- Не содержит состояния игрока — только "что можно купить и за сколько".
return {
Coins_1000 = {
Id = "Coins_1000",
DisplayName = "1000 монет",
Description = "Пополните запасы внутриигровой валюты",
Price = { RobuxProduct = "prod_coins_1k" }, -- имя, не ID!
Type = "CurrencyPack",
Metadata = { Amount = 1000 }
},
Sword_Fire = {
Id = "Sword_Fire",
DisplayName = "Огненный меч",
Description = "Наносит +5 урона и поджигает врагов",
Price = { Coins = 500 },
Type = "Tool",
Metadata = {
ToolAssetId = 123456789, -- rbxassetid ссылка на Tool в Toolbox или Inventory
DamageBonus = 5,
HasFireEffect = true
}
},
Skin_RedArmor = {
Id = "Skin_RedArmor",
DisplayName = "Красные доспехи",
Description = "Эксклюзивный внешний вид",
Price = { Coins = 1200 },
Type = "Appearance",
Metadata = {
ShirtTemplate = "rbxassetid://987000001",
PantsTemplate = "rbxassetid://987000002"
}
}
}
💡 Важно:
Price— таблица, чтобы в будущем можно было добавитьGems = 10без изменений в логике.RobuxProduct— символьное имя, а не числовой ID. Это позволяет избежать ошибок при переносе проекта (ID меняются между местами).Metadataстрого типизирован: система выдачи будет проверять наличие полей, а не их значения.
Шаг 3. Реализация адаптера к DataStore
ReplicatedStorage/Services/DataStoreAdapter.lua (ModuleScript)
local DataStoreService = game:GetService("DataStoreService")
-- Используем изолированное имя, чтобы не конфликтовать с другими играми
local STORE_NAME = "EconomyData_v2"
local RETRY_DELAY = 3
local MAX_RETRIES = 3
local DataStoreAdapter = {}
-- Загрузка данных игрока
function DataStoreAdapter.LoadAsync(player)
local userId = player.UserId
local success, data = pcall(function()
return DataStoreService:GetDataStore(STORE_NAME):GetAsync("Player_" .. userId)
end)
if not success then
warn("DataStore load failed for", player.Name, "-", data)
return nil
end
-- Если данных нет — создаём шаблон
if not data then
data = {
Coins = 0,
Inventory = {},
PurchaseHistory = {}
}
end
return data
end
-- Сохранение с повторными попытками
function DataStoreAdapter.SaveAsync(player, data)
local userId = player.UserId
local key = "Player_" .. userId
for i = 1, MAX_RETRIES do
local success, err = pcall(function()
DataStoreService:GetDataStore(STORE_NAME):SetAsync(key, data)
end)
if success then
return true
elseif i == MAX_RETRIES then
warn("DataStore save failed after retries for", player.Name, "-", err)
return false
else
task.wait(RETRY_DELAY)
end
end
end
return DataStoreAdapter
💡 Почему
SetAsync, а неUpdateAsync?
Для простоты этого гайда используетсяSetAsync. В продакшене обязательно перейдите наUpdateAsync, чтобы предотвратить перезапись при одновременных запросах. НоSetAsyncпроще для первого шага и демонстрирует базовую идею.
Шаг 4. Менеджер валюты и инвентаря
ReplicatedStorage/Services/CurrencyManager.lua
local DataStoreAdapter = require(script.Parent.DataStoreAdapter)
local CurrencyManager = {}
function CurrencyManager.GetBalance(player)
local data = CurrencyManager._GetPlayerData(player)
return data and data.Coins or 0
end
function CurrencyManager.AddCoins(player, amount)
if amount <= 0 then return false end
local data = CurrencyManager._GetPlayerData(player)
if not data then return false end
data.Coins = (data.Coins or 0) + amount
return CurrencyManager._SavePlayerData(player, data)
end
function CurrencyManager.SpendCoins(player, amount)
if amount <= 0 then return false end
local data = CurrencyManager._GetPlayerData(player)
if not data or (data.Coins or 0) < amount then
return false
end
data.Coins = data.Coins - amount
return CurrencyManager._SavePlayerData(player, data)
end
-- Внутренние методы (не экспортируются)
function CurrencyManager._GetPlayerData(player)
if not player:FindFirstChild("_EconomyData") then
local data = DataStoreAdapter.LoadAsync(player)
if data then
local folder = Instance.new("Folder")
folder.Name = "_EconomyData"
folder.Parent = player
-- Сохраняем в память игрока для быстрого доступа
folder:SetAttribute("Data", data)
end
end
return player._EconomyData and player._EconomyData:GetAttribute("Data")
end
function CurrencyManager._SavePlayerData(player, data)
player._EconomyData:SetAttribute("Data", data)
return DataStoreAdapter.SaveAsync(player, data)
end
return CurrencyManager
ReplicatedStorage/Services/InventorySystem.lua
local InventorySystem = {}
function InventorySystem.UnlockItem(player, itemId)
local data = require(script.Parent.CurrencyManager)._GetPlayerData(player)
if not data then return false end
if not data.Inventory then data.Inventory = {} end
data.Inventory[itemId] = data.Inventory[itemId] or {}
data.Inventory[itemId].Unlocked = true
return require(script.Parent.CurrencyManager)._SavePlayerData(player, data)
end
function InventorySystem.IsItemUnlocked(player, itemId)
local data = require(script.Parent.CurrencyManager)._GetPlayerData(player)
return data and data.Inventory and data.Inventory[itemId] and data.Inventory[itemId].Unlocked
end
return InventorySystem
💡 Замечание:
Оба модуля используют кэширование в_EconomyData, чтобы избежать частых вызововDataStore. Это повышает производительность. Синхронизация сDataStoreпроисходит только при изменении.
Шаг 5. Обработка транзакций
ReplicatedStorage/Services/TransactionProcessor.lua
local CurrencyManager = require(script.Parent.CurrencyManager)
local InventorySystem = require(script.Parent.InventorySystem)
local Catalog = require(script.Parent.Parent.Catalog.Items)
local TransactionProcessor = {}
-- Проверка уникальности транзакции
local function isDuplicate(player, transactionId)
local data = CurrencyManager._GetPlayerData(player)
if not data or not data.PurchaseHistory then return false end
for _, record in ipairs(data.PurchaseHistory) do
if record.TransactionId == transactionId then
return true
end
end
return false
end
-- Добавление записи в историю
local function recordTransaction(player, transactionId, itemId)
local data = CurrencyManager._GetPlayerData(player)
if not data.PurchaseHistory then data.PurchaseHistory = {} end
table.insert(data.PurchaseHistory, {
TransactionId = transactionId,
ItemId = itemId,
Timestamp = os.time()
})
end
-- Основная функция покупки
function TransactionProcessor.ProcessPurchase(player, itemId, transactionId)
if not player or not itemId or not transactionId then return false end
-- 1. Проверка дубликата
if isDuplicate(player, transactionId) then
warn("Duplicate transaction:", transactionId, "from", player.Name)
return false
end
-- 2. Получение описания товара
local item = Catalog[itemId]
if not item then
warn("Unknown item ID:", itemId)
return false
end
-- 3. Определение типа оплаты и валидация
local priceData = item.Price
local canAfford = false
if priceData.Coins then
-- Покупка за монеты
canAfford = CurrencyManager.GetBalance(player) >= priceData.Coins
elseif priceData.RobuxProduct then
-- Robux-покупка уже подтверждена Roblox — проверка не нужна
canAfford = true
else
warn("Item", itemId, "has no valid price")
return false
end
if not canAfford then return false end
-- 4. Выполнение операций
local success = false
-- Атомарное выполнение: всё или ничего
if priceData.Coins then
if not CurrencyManager.SpendCoins(player, priceData.Coins) then
return false
end
end
-- Выдача предмета по типу
if item.Type == "CurrencyPack" then
local amount = item.Metadata.Amount
success = CurrencyManager.AddCoins(player, amount)
elseif item.Type == "Tool" then
success = InventorySystem.UnlockItem(player, itemId)
elseif item.Type == "Appearance" then
success = InventorySystem.UnlockItem(player, itemId)
else
warn("Unsupported item type:", item.Type)
return false
end
if not success then return false end
-- 5. Запись в историю и сохранение
recordTransaction(player, transactionId, itemId)
return CurrencyManager._SavePlayerData(player, CurrencyManager._GetPlayerData(player))
end
return TransactionProcessor
💡 Ключевой момент:
Здесь нетRemoteEvent. Это чистая бизнес-логика, которую можно тестировать изолированно. Интеграция с событиями — на следующем шаге.
Шаг 6. Серверная точка входа
ServerScriptService/MainEconomySystem.lua
-- Создаём RemoteEvents, если их нет
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RemoteEvents = ReplicatedStorage:FindFirstChild("RemoteEvents")
if not RemoteEvents then
RemoteEvents = Instance.new("Folder")
RemoteEvents.Name = "RemoteEvents"
RemoteEvents.Parent = ReplicatedStorage
end
local BuyItem = ReplicatedStorage.RemoteEvents:FindFirstChild("BuyItem")
if not BuyItem then
BuyItem = Instance.new("RemoteEvent")
BuyItem.Name = "BuyItem"
BuyItem.Parent = ReplicatedStorage.RemoteEvents
end
local RequestBalance = ReplicatedStorage.RemoteEvents:FindFirstChild("RequestBalance")
if not RequestBalance then
RequestBalance = Instance.new("RemoteEvent")
RequestBalance.Name = "RequestBalance"
RequestBalance.Parent = ReplicatedStorage.RemoteEvents
end
-- Импорт сервисов
local TransactionProcessor = require(ReplicatedStorage.Services.TransactionProcessor)
local CurrencyManager = require(ReplicatedStorage.Services.CurrencyManager)
-- Обработка покупки
BuyItem.OnServerEvent:Connect(function(player, itemId, transactionId)
if not player:IsA("Player") then return end
local success = TransactionProcessor.ProcessPurchase(player, itemId, transactionId)
if success then
-- Уведомляем клиента об успехе и отправляем новый баланс
BuyItem:FireClient(player, "Success", { ItemId = itemId })
task.delay(0.1, function()
if player and player.Parent then
RequestBalance:FireClient(player, "Update", { Balance = CurrencyManager.GetBalance(player) })
end
end)
else
BuyItem:FireClient(player, "Failed", { Reason = "InvalidRequest" })
end
end)
-- Запрос баланса
RequestBalance.OnServerEvent:Connect(function(player)
if player and player:IsA("Player") then
local balance = CurrencyManager.GetBalance(player)
RequestBalance:FireClient(player, "Response", { Balance = balance })
end
end)
-- Поддержка Developer Products
local MarketplaceService = game:GetService("MarketplaceService")
-- Карта: имя продукта → внутренний ID
local PRODUCT_MAP = {
prod_coins_1k = "Coins_1000"
-- Добавьте другие по мере регистрации в DevHub
}
MarketplaceService.PromptPurchaseFinished:Connect(function(player, productId, purchased)
if not purchased or not player:IsA("Player") then return end
-- Получаем имя продукта по ID (в реальном проекте храните маппинг в ModuleScript)
local productName = nil
for name, id in pairs(script.Parent.Parent.DevProductMapping:GetAttribute("Products") or {}) do
if id == productId then
productName = name
break
end
end
if not productName then
warn("Unknown product ID:", productId)
return
end
local internalId = PRODUCT_MAP[productName]
if not internalId then
warn("No internal mapping for product:", productName)
return
end
local transactionId = "robux_" .. productName .. "_" .. os.time()
TransactionProcessor.ProcessPurchase(player, internalId, transactionId)
end)
-- Необязательно: выдача монет при входе (для теста)
game.Players.PlayerAdded:Connect(function(player)
task.delay(2, function()
if player and player.Parent then
CurrencyManager.AddCoins(player, 100) -- стартовый бонус
end
end)
end)
💡 Про Developer Products:
Чтобы это работало, нужно:
- Зарегистрировать Developer Product в DevHub.
- Запомнить его Product ID (число).
- Создать
ScriptвServerScriptServiceс именемDevProductMapping, содержащий:Это позволяет не хардкодить ID в основном коде.script:SetAttribute("Products", {
prod_coins_1k = 123456789 -- замените на реальный ID
})
Шаг 7. Клиентская часть: интерфейс магазина
StarterGui/ShopGUI/ShopFrame.lua (ScreenGui → Frame → LocalScript)
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")
local player = Players.LocalPlayer
local BuyEvent = ReplicatedStorage.RemoteEvents.BuyItem
local BalanceEvent = ReplicatedStorage.RemoteEvents.RequestBalance
-- Получаем элементы GUI (предполагаем, что они уже созданы в дизайнере)
local balanceLabel = script.Parent:WaitForChild("BalanceLabel")
local swordButton = script.Parent:WaitForChild("SwordButton")
local coinsButton = script.Parent:WaitForChild("CoinsButton")
-- Запрашиваем баланс при открытии
BalanceEvent:FireServer()
BalanceEvent.OnClientEvent:Connect(function(action, data)
if action == "Response" or action == "Update" then
balanceLabel.Text = "Монет: " .. (data.Balance or 0)
end
end)
-- Покупка меча за монеты
swordButton.MouseButton1Click:Connect(function()
local txId = "tx_" .. tick() .. "_" .. math.random(1000, 9999)
BuyEvent:FireServer("Sword_Fire", txId)
end)
-- Покупка монет за Robux
coinsButton.MouseButton1Click:Connect(function()
-- Получаем ID продукта по имени (из серверного маппинга)
local success, mapping = pcall(function()
return ReplicatedStorage:WaitForChild("DevProductMapping"):GetAttribute("Products")
end)
if success and mapping and mapping.prod_coins_1k then
MarketplaceService:PromptPurchase(player, mapping.prod_coins_1k)
else
warn("Developer product not configured")
end
end)
-- Обработка ответа от сервера
BuyEvent.OnClientEvent:Connect(function(status, data)
if status == "Success" then
print("Успешно куплено:", data.ItemId)
-- Можно показать эффект, звук, обновить инвентарь
elseif status == "Failed" then
if data.Reason == "InsufficientFunds" then
warn("Недостаточно монет")
else
warn("Покупка не удалась")
end
end
end)
💡 Важно для клиента:
- Никакого подсчёта баланса — только отображение.
- Никакой проверки «хватит ли денег» перед отправкой — это делает сервер.
tick()+math.randomдаёт уникальныйtransactionIdбез коллизий.
Шаг 8. Тестирование в Studio
- Запустите игру локально (
Playв Studio). - Откройте магазин — должен отобразиться баланс
100(стартовый бонус). - Нажмите «Купить меч» — баланс уменьшится на 500, предмет разблокируется.
- Перезайдите — баланс и инвентарь сохранятся (Roblox эмулирует DataStore локально).
- Для теста Robux-покупок:
- В Studio:
Test > Test Purchases. - Добавьте Developer Product (введите ID и название).
- Нажмите «Купить монеты» — появится окно подтверждения.
- После подтверждения — 1000 монет добавятся.
- В Studio:
✅ Если всё работает — система готова к деплою.
Что делать дальше (продвинутые шаги)
| Задача | Как реализовать |
|---|---|
| Поддержка нескольких валют | Расширить Price = { Coins = 100, Gems = 5 }, изменить CurrencyManager на работу с таблицей балансов ({ Coins = 500, Gems = 20 }). |
| Лидерборды по богатству | Использовать OrderedDataStore для хранения топ-100. |
| Возвраты (refunds) | Обрабатывать MarketplaceService.RefundOccurred. |
| A/B-тестирование цен | Хранить цены в Configuration-модуле, управляемом через Game.Settings или внешний JSON. |
| Модульные эффекты (например, надеть скин) | Создать AppearanceService, который слушает InventorySystem.ItemUnlocked через BindableEvent. |